Си (язык программирования)

Эта статья находится на начальном уровне проработки, в одной из её версий выборочно используется текст из источника, распространяемого под свободной лицензией
Материал из энциклопедии Руниверсалис
C
Класс языка процедурный
Тип исполнения компилируемый
Появился в 1972
Расширение файлов .c — для файлов кода, .h — для заголовочных файлов
Выпуск ISO/IEC 9899:2018 (5 июля 2018 года)
Система типов статическая слабая
Основные реализации GCC, Clang, TCC, Turbo C, Watcom, Oracle Solaris Studio C, Pelles C
Диалекты «K&R» C (1978)
ANSI C (1989)
C99 (1999)
C11 (2011)
Испытал влияние BCPL, B
Повлиял на C++, Objective-C, C#, Java, Nim
ISO/IEC 9899
Information technology — Programming languages — C
Издатель Международная организация по стандартизации (ISO)
Сайт www.iso.org
Комитет (разработчик) ISO/IEC JTC 1/SC 22
Сайт комитета Programming languages, their environments and system software interfaces
МКС (ICS) 35.060
Текущая редакция ISO/IEC 9899:2018
Предыдущие редакции ISO/IEC 9899:1990/COR2:1996
ISO/IEC 9899:1999/COR3:2007
ISO/IEC 9899:2011/COR1:2012
Стандартная библиотека
языка программирования С

Си (от лат. буквы C, англ. языка[⇨]) — компилируемый статически типизированный язык программирования общего назначения, разработанный в 1969—1973 годах сотрудником Bell Labs Деннисом Ритчи как развитие языка Би. Первоначально был разработан для реализации операционной системы UNIX, но впоследствии был перенесён на множество других платформ. Согласно дизайну языка, его конструкции близко сопоставляются типичным машинным инструкциям, благодаря чему он нашёл применение в проектах, для которых был свойственен язык ассемблера, в том числе как в операционных системах, так и в различном прикладном программном обеспечении для множества устройств — от суперкомпьютеров до встраиваемых систем. Язык программирования Си оказал существенное влияние на развитие индустрии программного обеспечения, а его синтаксис стал основой для таких языков программирования, как C++, C#, Java и Objective-C.

История

Язык программирования Си разрабатывался в период с 1969 по 1973 годы в лабораториях Bell Labs, и к 1973 году на этот язык была переписана большая часть ядра UNIX, первоначально написанного на ассемблере PDP-11/20. Название языка стало логическим продолжением старого языка «Би»[a], многие особенности которого были положены в основу.

По мере развития язык сначала стандартизировали как ANSI C, а затем этот стандарт был принят комитетом по международной стандартизации ISO как ISO C, ставший также известным под названием C90. В стандарте С99 язык получил новые возможности, такие как массивы переменной длины и встраиваемые функции. А в стандарте C11 в язык добавили реализацию потоков и поддержку атомарных типов. Однако с тех пор язык развивается медленно, и в стандарт C18 попали лишь исправления ошибок стандарта C11.

Общие сведения

Язык Си разрабатывался как язык системного программирования, для которого можно создать однопроходный компилятор. Стандартная библиотека также невелика. Как следствие данных факторов — компиляторы разрабатываются сравнительно легко[1]. Поэтому данный язык доступен на самых различных платформах. К тому же, несмотря на свою низкоуровневую природу, язык ориентирован на переносимость. Программы, соответствующие стандарту языка, могут компилироваться под различные архитектуры компьютеров.

Целью языка было облегчение написания больших программ с минимизацией ошибок по сравнению с ассемблером, следуя принципам процедурного программирования, но избегая всего, что может привести к дополнительным накладным расходам, специфичным для языков высокого уровня.

Основные особенности Си:

  • простая языковая база, из которой в стандартную библиотеку вынесены многие существенные возможности, вроде математических функций или функций работы с файлами;
  • ориентация на процедурное программирование;
  • система типов, предохраняющая от бессмысленных операций;
  • использование препроцессора для абстрагирования однотипных операций;
  • доступ к памяти через использование указателей;
  • небольшое число ключевых слов;
  • передача параметров в функцию по значению, а не по ссылке (передача по ссылке эмулируется с помощью указателей);
  • наличие указателей на функции и статические переменные;
  • области видимости имён;
  • структуры и объединения — определяемые пользователем собирательные типы данных, которыми можно манипулировать как одним целым.

В то же время в Си отсутствуют:

Часть отсутствующих возможностей может имитироваться встроенными средствами (например, сопрограммы можно имитировать с помощью функций setjmp и longjmp), часть добавляется с помощью сторонних библиотек (например, для поддержки многозадачности и для сетевых функций можно использовать библиотеки pthreads, sockets и тому подобные; существуют библиотеки для поддержки автоматической сборки мусора[2]), часть реализуется в некоторых компиляторах в виде расширений языка (например, вложенные функции в GCC). Существует несколько громоздкая, но вполне работоспособная методика, позволяющая реализовывать на Си механизмы ООП[3], базирующаяся на фактической полиморфности указателей в Си и поддержке в этом языке указателей на функции. Механизмы ООП, основанные на данной модели, реализованы в библиотеке GLib и активно используются во фреймворке GTK+. GLib предоставляет базовый класс GObject, возможности наследования от одного класса[4] и реализации множества интерфейсов[5].

После появления язык был хорошо принят, потому что он позволял быстро создавать компиляторы для новых платформ, а также позволял программистам довольно точно представлять, как выполняются их программы. Благодаря близости к языкам низкого уровня программы на Си работали эффективнее написанных на многих других языках высокого уровня, и лишь оптимизированный вручную код на ассемблере мог работать ещё быстрее, потому что давал полный контроль над машиной. На сегодняшний день развитие компиляторов и усложнение процессоров привело к тому, что вручную написанный ассемблерный код (кроме разве что очень коротких программ) практически не выигрывает по сравнению с кодом, генерируемым компиляторами, при этом Си продолжает оставаться одним из наиболее эффективных языков высокого уровня.

Синтаксис и семантика

Лексемы

Алфавит языка

В языке используются все символы латинского алфавита, цифры и некоторые специальные символы[6].

Состав алфавита[6]
Символы латинского алфавита

A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z
a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z

Цифры 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Специальные символы , (запятая), ;,. (точка), +, -, *, ^, & (амперсанд), =, ~ (тильда), !, /, <, >, (, ), {, }, [, ], |, %, ?, ' (апостроф), " (кавычки), : (двоеточие), _ (знак подчёркивания), \, #

Из допустимых символов формируются лексемы — предопределённые константы, идентификаторы и знаки операций. В свою очередь, лексемы являются частью выражений; а из выражений составляются инструкции и операторы.

При трансляции программы на Си из программного кода выделяются лексемы максимальной длины, содержащие допустимые символы. Если в программе имеется недопустимый символ, то лексический анализатор (или компилятор) выдаст ошибку, и трансляция программы окажется невозможной.

Символ # не может быть частью никакой лексемы и используется в препроцессоре[⇨].

Идентификаторы

Допустимый идентификатор — это слово, в состав которого могут входить символы латинского алфавита, цифры и знак подчёркивания[7]. Идентификаторы даются операторам, константам, переменным, типам и функциям.

В качестве идентификаторов программных объектов не могут использоваться идентификаторы ключевых слов и встроенные идентификаторы. Существуют и зарезервированные идентификаторы, на использование которых компилятор не выдаст ошибок, но которые в будущем могут стать ключевыми словами, что повлечёт за собой несовместимость.

Встроенный идентификатор только один — __func__, который определяется как константная строка, неявно объявляемая в каждой функции и содержащая её название[7].

Литеральные константы

Специально оформленные литералы в Си принято называть константами. Литеральные константы могут быть целочисленными, вещественными, символьными[8] и строковыми[9].

Целые числа по умолчанию задаются в десятичной системе счисления. Если указан префикс 0x, то — в шестнадцатеричной системе. Префикс в виде цифры 0 указывает, что число задаётся в восьмеричной системе. Суффикс определяет минимальный размер типа константы, а также определяет, является ли число знаковым или беззнаковым. В качестве итогового типа берётся такой минимально возможный, в котором данную константу можно представить[10].

Порядок назначения типов данных целым константам согласно их значению[10]
Суффикс Для десятичных Для восьмеричных и шестнадцатеричных
Нет int

long

long long

int

unsigned int

long

unsigned long

long long

unsigned long long

u или U unsigned int

unsigned long

unsigned long long

unsigned int

unsigned long

unsigned long long

l или L long

long long

long

unsigned long

long long

unsigned long long

u или U вместе с l или L unsigned long

unsigned long long

unsigned long

unsigned long long

ll или LL long long long long

unsigned long long

u или U вместе с ll или LL unsigned long long unsigned long long
Примеры записи вещественного числа 1.5
Десятичный

формат

С экспонентой Шестнадцатеричный

формат

1.5 1.5e+0 0x1.8p+0
15e-1 0x3.0p-1
0.15e+1 0x0.cp+1

Константы вещественных чисел по умолчанию имеют тип double. При указании суффикса f константе назначается тип float, а при указании l или L — long double. Константа будет считаться вещественной, если в ней присутствует знак точки, либо буквы p или P в случае шестнадцатеричной записи с префиксом 0x. Десятичная запись может включать экспоненту, указываемую после букв e или E. В случае шестнадцатеричной записи экспонента указывается после букв p или P в обязательном порядке, что отличает вещественные шестнадцатеричные константы от целых. В шестнадцатеричном виде экспонента является степенью числа 2[11].

Символьные константы заключаются в одинарные кавычки ('), а префикс задаёт как тип данных символьной константы, так и кодировку, в которой символ будет представлен. В Си символьная константа без префикса имеет тип int[12], в отличие от C++, в котором символьной константе соответствует char.

Префиксы символьных констант[12]
Префикс Тип данных Кодировка
Нет int ASCII
u char16_t Кодировка 16-битных многобайтовых строк
U char32_t Кодировка 32-битных многобайтовых строк
L wchar_t Кодировка широких строк

Строковые литералы заключаются в двойные кавычки и могут иметь префикс, определяющий тип данных строки и её кодировку. Строковые литералы представляют собой обычные массивы. При этом в многобайтовых кодировках, таких как UTF-8, один символ может занимать более одного элемента массива. По факту строковые литералы являются константными[13], но в отличие от C++ их типы данных не содержат модификатор const.

Префиксы строковых констант[14]
Префикс Тип данных Кодировка
Нет char * ASCII или многобайтовая кодировка
u8 char * UTF-8
u char16_t * 16-битная многобайтовая кодировка
U char32_t * 32-битная многобайтовая кодировка
L wchar_t * Кодировка широких строк

Несколько подряд идущих строковых констант, разделённых пробельными символами или переводами строк объединяются в одну строку при компиляции, что часто используется для оформления кода строки путём разделения частей строковой константы по разным строкам для повышения читабельности[15].

Именованные константы

Сравнение способов задания констант[16]
Макрос
#define BUFFER_SIZE 1024
Анонимное
перечисление
enum {
    BUFFER_SIZE = 1024
};
Переменная
в роли
константы
const int
buffer_size = 1024;
extern const int
buffer_size;

В языке Си для задания констант принято использовать макроопределения, объявляемые с помощью директивы препроцессора[⇨] #define[16]:

#define имя константы [значение]

Введённая таким образом константа будет действовать в области своей видимости, начиная с момента задания константы и до конца программного кода или до тех пор, пока действие заданной константы не будет отменено директивой #undef:

#undef имя константы

Как и для всякого макроса, для именованной константы происходит автоматическая подстановка значения константы в программном коде всюду, где употреблено имя константы. Поэтому при объявлении внутри макроса целых или вещественных чисел может понадобиться явно указывать тип данных с помощью соответствующего суффикса литерала, иначе число по умолчанию будет иметь тип int в случае целого или тип double — в случае вещественного.

Для целых чисел существует другой способ создания именованных констант — через перечисления оператора enum[16][⇨]. Однако данный метод подходит только для типов, размером меньших либо равных типу int, и не используется в стандартной библиотеке[17].

Также можно создавать константы в виде переменных с квалификатором const, но в отличие от двух других способов, такие константы потребляют память, на них можно получить указатель, и их нельзя использовать на этапе компиляции[16]:

  • для указания размера битовых полей,
  • для задания размера массива (за исключением массивов переменной длины),
  • для задания значения элемента перечисления,
  • в качестве значения оператора case.

Ключевые слова

Ключевые слова — это идентификаторы, предназначенные для выполнения той или иной задачи на этапе компиляции, либо для подсказок и указаний компилятору.

Ключевые слова языка Си[18]
Ключевые слова Назначение Стандарт
sizeof Получение размера объекта на этапе компиляции C89
typedef Задание альтернативного имени типу
auto, register Подсказки компилятору по месту хранения переменных
extern Указание компилятору искать объект вне текущего файла
static Объявление статического объекта
void Маркер отсутствия значения; в указателях означает произвольные данные
char, short,int, long Целочисленные типы и модификаторы их размера
signed, unsigned Модификаторы целочисленных типов, определяющие их как знаковые или беззнаковые
float, double Вещественные типы данных
const Модификатор типа данных, указывающий компилятору, что переменные этого типа доступны только для чтения
volatile Указание компилятору на возможность изменения значения переменной извне
struct Тип данных в виде структуры с набором полей
enum Тип данных, хранящий одно из набора целочисленных значений
union Тип данных, в котором можно хранить данные в представлениях разных типов данных
do, for, while Операторы цикла
if, else Условный оператор
switch, case, default Оператор выбора по целочисленному параметру
break, continue Операторы прерывания цикла
goto Оператор безусловного перехода
return Возврат из функции
inline Объявление встраиваемой функции C99[19]
restrict Объявление указателя, который ссылается на блок памяти, на который не ссылается никакой другой указатель
_Bool[b] Булев тип данных
_Complex[c], _Imaginary[d] Типы, используемые для вычислений с комплексными числами
_Atomic Модификатор типа, делающий его атомарным C11
_Alignas[e] Явное задание выравнивания в байтах для типа данных
_Alignof[f] Получение выравнивания для заданного типа данных на этапе компиляции[⇨]
_Generic Выбор одного из набора значений на этапе компиляции, исходя из контролируемого типа данных
_Noreturn[g] Указание компилятору, что функция не может завершаться нормальным образом (то есть по return)
_Static_assert[h] Указание утверждений, проверяемых на этапе компиляции
_Thread_local[i] Объявление локальной для потока переменной

Зарезервированные идентификаторы

Помимо ключевых слов стандарт языка определяет зарезервированные идентификаторы, использование которых может привести к несовместимости с будущими версиями стандарта. Зарезервированными являются все, за исключением ключевых, слова, начинающиеся со знака подчёркивания (_), после которого идёт либо заглавная буква (AZ), либо другой знак подчёркивания[20]. В стандартах С99 и С11 часть таких идентификаторов была использована под новые ключевые слова языка.

В области видимости файла зарезервировано использование любых имён, начинающихся со знака подчёркивания (_)[20], то есть со знака подчёркивания допускается именовать типы, константы и переменные, объявленные в рамках какого-либо блока инструкций, например, внутри функций.

Также зарезервированными идентификаторами являются все макросы стандартной библиотеки и связываемые на этапе линковки названия из неё[20].

Использование зарезервированных идентификаторов в программах стандарт определяет как неопределённое поведение. Попытка отмены любого стандартного макроса через #undef также повлечёт за собой неопределённое поведение[20].

Комментарии

Текст программы на Си может содержать фрагменты, которые не являются частью программного кода, — комментарии. Комментарии специальным образом помечаются в тексте программы и пропускаются при компиляции.

Первоначально, в стандарте C89, были доступны встраиваемые комментарии, которые могли помещаться между последовательностями символов /* и */. При этом невозможно вложить один комментарий в другой, поскольку первая встреченная последовательность */ завершит комментарий, а текст, следующий непосредственно за обозначением */, будет воспринят компилятором как исходный текст программы.

Следующий стандарт, C99, ввёл ещё один способ оформления комментариев: комментарием считается текст, начинающийся с последовательности символов // и заканчивающийся концом строки[19].

Комментарии часто используются для самодокументирования исходного кода, поясняя работу сложных частей, описывая назначение тех или иных файлов, а также описывая правила использования и работу тех или иных функций, макросов, типов данных и переменных. Существуют постпроцессоры, которые умеют преобразовывать специально оформленные комментарии в документацию. Среди таких постпроцессоров с языком Си умеет работать система документирования Doxygen.

Операторы

Операторы, применяемые в выражениях, представляют собой некоторую операцию, которая выполняется над операндами и которая возвращает вычисленное значение — результат выполнения операции. В качестве операнда может выступать константа, переменная, выражение или вызов функции. Оператор может представлять собой специальный символ, набор специальных символов или служебное слово. Операторы различают по количеству задействованных операндов, а именно — различают унарные операторы, бинарные операторы и тернарные операторы.

Унарные операторы

Унарные операторы выполняют операцию над единственным аргументом и имеют следующий формат операции:

[оператор] [операнд]

Операции постфиксного инкремента и декремента имеют обратный формат:

[операнд] [оператор]
Унарные операторы языка Си[21]
+ Унарный плюс ~ Взятие обратного кода & Взятие адреса ++ Префиксный или постфиксный инкремент sizeof Получение количества байт, занимаемого объектом в памяти; может использоваться и как операция, и как оператор
- Унарный минус ! логическое отрицание * Разыменовывание указателя -- Префиксный или постфиксный декремент _Alignof Получение выравнивания для заданного типа данных

Операторы инкремента и декремента, в отличие от остальных унарных операторов, изменяют значение своего операнда. Префиксный оператор сначала изменяет значение, а затем возвращает его. Постфиксный же сначала возвращает значение, а только потом его изменяет.

Бинарные операторы

Бинарные операторы располагаются между двумя аргументами и осуществляют операцию над ними:

[операнд] [оператор] [операнд]
Базовые бинарные операторы[22]
+ Сложение % Взятие остатка от деления << Поразрядный сдвиг влево > Больше == Равно
- Вычитание & Поразрядное И >> Поразрядный сдвиг вправо < Меньше != Не равно
* Умножение | Поразрядное ИЛИ && Логическое И >= Больше либо равно
/ Деление ^ Поразрядное исключающее ИЛИ || Логическое ИЛИ <= Меньше либо равно

Также к бинарным операторам в Си относятся лево-присваивающие операторы, которые производят операцию над левым и правым аргументом и заносят результат в левый аргумент.

Лево-присваивающие бинарные операторы[23]
= Присвоение значения правого аргумента левому %= Остаток от деления левого операнда на правый ^= Поразрядное исключающее ИЛИ правого операнда к левому
+= Прибавление к левому операнду правого /= Деление левого операнда на правый <<= Поразрядный сдвиг левого операнда влево на количество бит, заданное правым операндом
-= Вычитание из левого операнда правого &= Поразрядное И правого операнда к левому >>= Поразрядный сдвиг левого операнда вправо на количество бит, заданное правым операндом
*= Умножение левого операнда на правый |= Порязрядное ИЛИ правого операнда к левому

Тернарные операторы

В Си имеется единственный тернарный оператор — сокращённый условный оператор, который имеет следующий вид:

[условие] ? [выражение1] : [выражение2]

Сокращённый условный оператор имеет три операнда:

  • [условие] — логическое условие, которое проверяется на истинность,
  • [выражение1] — выражение, значение которого возвращается в качестве результата выполнения операции, если условие истинно;
  • [выражение2] — выражение, значение которого возвращается в качестве результата выполнения операции, если условие ложно.

Оператором в данном случае является сочетание знаков ? и :.

Выражения

Выражение — это упорядоченный набор операций над константами, переменными и функциями. Выражения содержат операции, состоящие из операндов и операторов[⇨]. Порядок выполнения операций зависит от формы записи и от приоритета выполнения операций. У каждого выражения имеется значение — результат выполнения всех операций, входящих в выражение. В ходе вычисления выражения в зависимости от операций могут изменяться значения переменных, а также могут исполняться функции, если их вызовы присутствуют в выражении.

Среди выражений выделяют класс лево-допустимых выражений — выражений, которые могут присутствовать слева от знака присваивания.

Приоритет выполнения операций

Приоритет операций определяется стандартом и задаёт порядок, в котором операции будут производиться. Операции в Си выполняются в соответствии приведённой ниже таблице приоритетов[24][25].

Приоритет Лексемы Операция Класс Ассоциативность
1 a[индекс] Обращение по индексу постфиксный слева направо
f(аргументы) Вызов функции
. Доступ к полю
-> Доступ к полю по указателю
++ -- Положительное и отрицательное приращение
(имя типа) {инициализатор} Составной литерал (C99)
(имя типа) {инициализатор,}
2 ++ -- Положительное и отрицательное префиксные приращения унарный справа налево
sizeof Получение размера
_Alignof[f] Получение выравнивания (C11)
~ Побитовое НЕ
! Логическое НЕ
- + Указание знака (минус или плюс)
& Получение адреса
* Обращение по указателю (разыменовывание)
(имя типа) Приведение типа
3 * / % Умножение, деление и получение остатка бинарный слева направо
4 + - Сложение и вычитание
5 << >> Сдвиг влево и вправо
6 < > <= >= Операции сравнения
7 == != Проверка на равенство или неравенство
8 & Побитовое И
9 ^ Побитовое исключающее ИЛИ
10 | Побитовое ИЛИ
11 && Логическое И
12 || Логическое ИЛИ
13 ? : Условие тернарный справа налево
14 = Присвоение значения бинарный
+= -= *= /= %= <<= >>= &= ^= |= Операции изменения левого значения
15 , Последовательное вычисление слева направо

Приоритеты операций в Си не всегда себя оправдывают и иногда приводят к интуитивно трудно предсказуемым результатам. Например, поскольку унарные операторы имеют ассоциативность справа налево, то вычисление выражения *p++ приведёт к увеличению указателя с последующим разыменовыванием (*(p++)), а не к увеличению значения по указателю ((*p)++). Поэтому в случае сложных для понимания ситуаций рекомендуется явно группировать выражения с помощью скобок[25].

Другой важной особенностью языка Си является то, что вычисление значений аргументов, передаваемых в вызов функции не является последовательным[26], то есть запятая, разделяющая аргументы, не соответствует последовательному вычислению из таблицы приоритетов. В следующем примере вызовы функций, указываемые в качестве аргументов другой функции, могут идти в произвольном порядке:

int x;
x = compute(get_arg1(), get_arg2()); // первым может быть вызов get_arg2()

Также нельзя полагаться на приоритет операций в случае наличия побочных эффектов, появляющихся в ходе вычисления выражения, поскольку это будет приводить к неопределённому поведению[26].

Точки следования и побочные эффекты

Приложение C стандарта языка определяет набор точек следования, в которых гарантируется отсутствие текущих побочных эффектов от вычислений. То есть точка следования — это этап вычислений, который разделяет вычисление выражений между собой так, что произошедшие до точки следования вычисления, включая побочные эффекты, уже закончились, а после точки следования — ещё не начинались[27]. Побочным эффектом может быть изменение значения переменной в ходе вычисления выражения. Изменение значения, участвующего в вычислениях, вместе с побочным изменением этого же значения до следующей точки следования будет приводить к неопределённому поведению. То же самое будет, если происходит два или более побочных изменений одного и того же значения, участвующего в вычислениях[26].

Точки следования, определённые стандартом[26]
Точка следования Событие до Событие после
Вызов функции Вычисление указателя на функцию и её аргументов Вызов функции
Операторы логического И (&&), ИЛИ (||) и последовательное вычисление (,) Вычисление первого операнда Вычисление второго операнда
Сокращённый оператор условия (?:) Вычисление операнда, выступающего условием Вычисление 2-го или 3-го операндов
Между двумя полными выражениями (не вложенными) Одно полное выражение Следующее полное выражение
Законченный полный описатель
Сразу перед возвратом из библиотечной функции
После каждого преобразования, связанного со спецификатором форматированного ввода-вывода
Сразу перед и сразу после каждого вызова функции сравнения, а также между вызовом функции сравнения и любыми перемещениями, выполняемыми над передаваемыми в функцию сравнения аргументами

Полными выражениями считаются[26]:

  • инициализатор, не являющийся частью составного литерала;
  • обособленное выражение;
  • выражение, указанное в качестве условия условного оператора (if) или оператора выбора (switch);
  • выражение, указанное в качестве условия цикла while с предусловием или с постусловием;
  • каждый из параметров цикла for, если таковой указан;
  • выражение оператора return, если таковое указано.

В следующем примере переменная изменяется трижды между точками следования, что приводит к неопределённому результату:

int i = 1;    // Описатель - первая точка следования, полное выражение - вторая
i += ++i + 1; // Полное выражение - третья точка следования
printf("%d\n", i); // Может быть выведено как 4, так и 5

Другие простые примеры неопределённого поведения, которого необходимо избегать:

i = i++ + 1; // неопределённое поведение
i = ++i + 1; // тоже неопределённое поведение

printf("%d, %d\n", --i, ++i); // неопределённое поведение
printf("%d, %d\n", ++i, ++i); // тоже неопределённое поведение

printf("%d, %d\n", i = 0, i = 1); // неопределённое поведение
printf("%d, %d\n", i = 0, i = 0); // тоже неопределённое поведение

a[i] = i++; // неопределённое поведение
a[i++] = i; // тоже неопределённое поведение

Управляющие операторы

Управляющие операторы предназначены для осуществления действий и для управления ходом выполнения программы. Несколько идущих подряд операторов образуют последовательность операторов.

Пустой оператор

Самая простая языковая конструкция — это пустое выражение, называемое пустым оператором[28]:

;

Пустой оператор не совершает никаких действий и может находиться в любом месте программы. Обычно используется в циклах с отсутствующим телом[29].

Инструкции

Инструкция — это некое элементарное действие:

(выражение);

Действие этого оператора заключается в выполнении указанного в теле оператора выражения.

Несколько идущих подряд инструкций образуют последовательность инструкций.

Блок инструкций

Инструкции могут быть сгруппированы в специальные блоки следующего вида:

{

(последовательность инструкций)

},

Блок инструкций, также иногда называемый составным оператором, ограничивается левой фигурной скобкой ({) в начале и правой фигурной скобкой (}) — в конце.

В функциях[⇨] блок инструкций обозначает тело функции и является частью определения функции. Также составной оператор может использоваться в операторах циклов, условия и выбора.

Условные операторы

В языке существует два условных оператора, реализующих ветвление программы:

  • оператор if, содержащий проверку одного условия,
  • и оператор switch, содержащий проверку нескольких условий.

Самая простая форма оператора if

if((условие)) (оператор)
(следующий оператор)

Оператор if работает следующим образом:

  • если выполнено условие, указанное в скобках, то выполняется первый оператор, и затем выполняется оператор, указанный после оператора if.
  • если условие, указанное в скобках, не выполнено, то сразу выполняется оператор, указанный после оператора if.

В частности, следующий ниже код, в случае выполнения заданного условия, не будет выполнять никаких действий, поскольку, фактически, выполняется пустой оператор:

if((условие)) ;

Более сложная форма оператора if содержит ключевое слово else:

if((условие)) (оператор)
else (альтернативный оператор)
(следующий оператор)

Здесь, если условие, указанное в скобках, не выполнено, то выполняется оператор, указанный после ключевого слова else.

Несмотря на то, что стандарт допускает указание тела операторов if или else одной строкой, это считается плохим стилем, снижающим читабельность кода. В качестве тела рекомендуется всегда указывать блок инструкций с помощью фигурный скобок[30].

Операторы выполнения цикла

Цикл — это фрагмент программного кода, содержащий

  • условие выполнения цикла — условие, которое постоянно проверяется;
  • и тело цикла — простой или составной оператор, выполнение которого зависит от условия цикла.

В соответствии с этим, различают два вида циклов:

  • цикл с предусловием, где сначала проверяется условие выполнения цикла, и, если условие выполнено, то выполняется тело цикла;
  • цикл с постусловием, где проверка условия продолжения цикла происходит после исполнения тела цикла.

Цикл с постусловием гарантирует, что тело цикла выполнится по крайней мере один раз.

В языке Си предусмотрено два варианта циклов с предусловием: while и for.

while(условие) [тело цикла]
for( блок инициализации;условие;оператор) [тело цикла],

Цикл for ещё называется параметрическим, он эквивалентен следующему блоку операторов:

[блок инициализации]
while(условие)
{
[тело цикла]
[оператор]
}

В обычной ситуации блок инициализации содержит задание начального значения переменной, которая называется переменной цикла, а оператор, который выполняется сразу после тела цикла, меняет значения используемой переменной, условие содержит сравнение значения используемой переменной цикла с некоторым заранее заданным значением, и, как только сравнение перестаёт выполняться, цикл прерывается, и начинает выполняться программный код, следующий сразу за оператором цикла.

У цикла do-while условие указывается после тела цикла:

do [тело цикла] while( условие)

Условие цикла — это логическое выражение. Однако неявное приведение типов позволяет использовать в качестве условия цикла арифметическое выражение. Это позволяет организовать так называемый «бесконечный цикл»:

while(1);

То же самое можно сделать и с применением оператора for:

for(;;);

На практике такие бесконечные циклы обычно используются совместно с операторами break, goto или return, которые осуществляют прерывание работы цикла разными способами.

Как и для оператора условия, использование однострочного тела без заключения его в блок инструкций с помощью фигурных скобок считается плохим стилем, снижающим читабельность кода[30].

Операторы безусловного перехода

Операторы безусловного перехода позволяют прервать выполнение любого блока вычислений и перейти в другое место программы в рамках текущей функции. Операторы безусловного перехода обычно используются совместно с условными операторами.

goto [метка],

Метка — это некоторый идентификатор, передаёт управление тому оператору, который помечен в программе указанной меткой:

[метка] : [оператор]

Если указанная метка отсутствует в программе или если существует несколько операторов с одной и той же меткой, компилятор сообщает об ошибке.

Передача управления возможна только в пределах той функции, где используется оператор перехода, следовательно, при помощи оператора goto нельзя передать управление в другую функцию.

Другие операторы перехода связаны с циклами и позволяют прервать выполнения тела цикла:

  • оператор break немедленно прерывает выполнение тела цикла, и происходит передача управления на оператор, следующий непосредственно сразу за циклом;
  • оператор continue прерывает выполнение текущей итерации цикла и инициирует попытку перехода к следующей.

Оператор break также может прерывать работу оператора switch, поэтому внутри оператора switch, запущенного в цикле, оператор break не сможет прервать работу цикла. Указанный в теле цикла, он прерывает работу ближайшего вложенного цикла.

Оператор continue может быть использован только внутри операторов do, while и for. У циклов while и do-while оператор continue вызывает проверку условия цикла, а в случае цикла for — исполнение оператора, заданного в 3-м параметре цикла, перед проверкой условия продолжения цикла.

Оператор возврата из функции

Оператор return прерывает выполнение той функции, в которой использован. Если функция не должна возвращать значение, то используется вызов без возвращаемого значения:

return;

Если функция должна возвращать какое-либо значение, то после оператора указывается возвращаемое значения:

return[значение];

Если после оператора возврата в теле функции имеются ещё какие-то операторы, то эти операторы никогда не будут выполняться, и в этом случае компилятор может выдать предупреждение. Однако после оператора return могут указываться инструкции для альтернативного завершения функции, например, по ошибке, а переход к этим операторам можно осуществлять с помощью оператора goto согласно каким-либо условиям[⇨].

Переменные

При объявлении переменной указывается её тип[⇨] и название, а также может указываться начальное значение:

[описатель] [имя] ;

или

[описатель] [имя] = [инициализатор] ;,

где

  • [описатель] — тип переменной и предшествующие типу необязательные модификаторы;
  • [имя] — имя переменной;
  • [инициализатор] — начальное значение переменной, присваиваемое при её создании.

Если переменной не присвоено начальное значение, то в случае глобальной переменной её значение заполняется нулями, а для локальной переменной начальное значение будет неопределённым.

В описателе переменной можно обозначать переменную как глобальную, но ограниченную областью видимости файла или функции, с помощью ключевого слова static. Если переменная объявлена глобальной без ключевого слова static, то обращаться к ней возможно и из других файлов, где требуется объявить данную переменную без инициализатора, но с ключевым словом extern. Адреса таких переменных определяются на этапе компоновки.

Функции

Функция — это самостоятельный фрагмент программного кода, который может многократно использоваться в программе. Функции могут иметь аргументы и могут возвращать значения. Также функции могут иметь побочные эффекты при своём исполнении: изменение глобальных переменных, работа с файлами, взаимодействие с операционной системой или оборудованием[27].

Для того, чтобы задать функцию в Си, необходимо её объявить:

  • сообщить имя (идентификатор) функции,
  • перечислить входные параметры (аргументы)
  • и указать тип возвращаемого значения.

Также необходимо привести определение функции, которое содержит блок операторов, реализующих поведение функции.

Отсутствие объявления определённой функции является ошибкой, если функция используется вне области видимости определения, что, в зависимости от реализации, приводит к выдаче сообщений или предупреждений.

Для вызова функции достаточно указать её имя с параметрами, указанными в скобках. При этом адрес точки вызова помещается в стек, создаются и инициализируются переменные, отвечающие за параметры функции, и передаётся управление коду, реализующему вызываемую функцию. После выполнения функции происходит освобождение памяти, выделенной при вызове функции, возврат в точку вызова и, если вызов функции является частью некоторого выражения, передача в точку возврата вычисленного внутри функции значения.

Если после функции не указаны скобки, то компилятор интерпретирует это как получение адреса функции. Адрес функции можно заносить в указатель и в последующем вызывать функцию посредством указателя на неё, что активно используется, например, в системах плагинов[31].

С помощью ключевого слова inline можно помечать функции, вызовы которых требуется исполнять как можно быстрее. Компилятор может подставлять код таких функций непосредственно в точку их вызова[32]. С одной стороны, это увеличивает объём исполняемого кода, но, с другой, — позволяет экономить время его выполнения, поскольку не используется дорогостоящая по времени операция вызова функции. Однако из-за особенностей построения архитектуры компьютеров, встраивание функций может приводить как к ускорению, так и к замедлению работы приложения в целом. Тем не менее во многих случаях встраиваемые функции являются предпочтительной заменой макросам[33].

Объявление функции

Объявление функции имеет следующий формат:

[описатель] [имя] ( [список] );,

где

  • [описатель] — описатель типа возвращаемого функцией значения;
  • [имя] — имя функции (уникальный идентификатор функции);
  • [список] — список (формальных) параметров функции или void при их отсутствии[34].

Признаком объявления функции является символ «;», таким образом, объявление функции — это инструкция.

В самом простом случае [описатель] содержит указание на конкретный тип возвращаемого значения. Функция, которая не должна возвращать никакого значения, объявляется как имеющая тип void.

При необходимости в описателе могут присутствовать модификаторы, задаваемые с помощью ключевых слов:

  • extern указывает на то, что определение функции находится в другом модуле[⇨];
  • static задаёт статическую функцию, которая может быть использована только в текущем модуле.

Список параметров функции задаёт сигнатуру функции.

Си не допускает объявление нескольких функций, имеющих одно и то же имя, перегрузка функций не поддерживается[35].

Определение функции

Определение функции имеет следующий формат:

[описатель] [имя] ( [список] ) [тело]

Где [описатель], [имя] и [список] — те же, что и в объявлении, а [тело] — это составной оператор, который представляет собою конкретную реализацию функции. Компилятор различает определения одноимённых функций по их сигнатуре, и таким образом (по сигнатуре) устанавливается связь между определением и соответствующим ему объявлением.

Тело функции имеет следующий вид:

{
[последовательность операторов]
return ([возвращаемое значение]) ;
}

Возврат из функции осуществляется с помощью оператора return[⇨], у которого либо указывается возвращаемое значение, либо не указывается, в зависимости от возвращаемого функцией типа данных. В редких случаях функция может быть помечена как не делающая возврат с помощью макроса noreturn из заголовочного файла stdnoreturn.h, в таких случаях оператор return не требуется. Например, подобным образом можно помечать функции, безусловно вызывающие внутри себя abort()[32].

Вызов функции

Вызов функции заключается в выполнении следующих действий:

  • сохранение точки вызова в стеке;
  • автоматическое выделение памяти[⇨] под переменные, соответствующие формальным параметрам функции;
  • инициализация переменных значениями переменных (фактических параметров функции), переданных в функцию при её вызове, а также инициализация тех переменных, для которых в объявлении функции указаны значения по умолчанию, но для которых при вызове не были указаны соответствующие им фактические параметры;
  • передача управления в тело функции.

В зависимости от реализации, компилятор либо строго следит за тем, чтобы тип фактического параметра совпадал с типом формального параметра, либо, если существует такая возможность, осуществляет неявное преобразование типа, что, очевидно, приводит к побочным эффектам.

Если в функцию передаётся переменная, то при вызове функции создаётся её копия (в стеке выделяется память и копируется значение). Например, передача структуры в функцию вызовет копирование всей структуры целиком. Если же передаётся указатель на структуру, то копируется только значение указателя. Передача в функцию массива также вызывает лишь копирование указателя на его первый элемент. При этом для явного обозначения того, что на вход функции принимается адрес начала массива, а не указатель на единичную переменную, вместо объявления указателя после названия переменной можно поставить квадратные скобки, например:

void example_func(int array[]); // array — указатель на первый элемент массива типа int

Си допускает вложенные вызовы. Глубина вложенности вызовов имеет очевидное ограничение, связанное с размером выделяемого программе стека. Поэтому в реализациях Си устанавливается некое предельное значение для глубины вложенности.

Частный случай вложенного вызова — это вызов функции внутри тела вызываемой функции. Такой вызов называется рекурсивным, и применяется для организации единообразных вычислений. Учитывая естественное ограничение на вложенные вызовы, рекурсивную реализацию заменяют на реализацию при помощи циклов.

Типы данных

Примитивные типы

Целые числа

Размер целочисленных типов данных варьируется от не менее 8 до не менее 32 бит. Стандарт C99 увеличивает максимальный размер целого числа — не менее 64 бит. Целочисленные типы данных используются для хранения целых чисел (тип char также используется для хранения ASCII-символов). Все размеры диапазонов представленных ниже типов данных минимальны и на отдельно взятой платформе могут быть больше[36].

Как следствие минимальных размеров типов стандарт требует, чтобы для размеров целочисленных типов выполнялось условие:

1 = sizeof(char)sizeof(short)sizeof(int)sizeof(long)sizeof(long long).

Таким образом, размеры некоторых типов по количеству байт могут совпадать, если будет удовлетворяться условие по минимальному количеству бит. Даже char и long могут иметь одинаковый размер, если один байт будет занимать 32 бита или более, но такие платформы будут очень редки или не будут существовать. Стандарт гарантирует, что тип char всегда равен 1 байту. Размер байта в битах определяется константой CHAR_BIT из заголовочного файла limits.h, у POSIX-совместимых систем равен 8 битам[37].

Минимальный диапазон значений целых типов по стандарту определяется с -(2N-1-1) по 2N-1-1 для знаковых типов и с 0 по 2N — для беззнаковых, где N — разрядность типа. Реализация компиляторов может расширять этот диапазон по своему усмотрению. На практике для знаковых типов чаще используется диапазон с -2N-1 по 2N-1-1. Минимальное и максимальное значения каждого типа указывается в файле limits.h в виде макроопределений.

Отдельное внимание стоит уделить типу char. Формально это отдельный тип, но фактически char эквивалентен либо signed char, либо unsigned char, в зависимости от компилятора[38].

Для того, чтобы избежать путаницы между размерами типов стандарт C99 ввел новые типы данных, описанные в файле stdint.h. Среди них такие типы как: intN_t, int_leastN_t, int_fastN_t, где N = 8, 16, 32 или 64. Приставка least- обозначает минимальный тип, способный вместить N бит, приставка fast- обозначает тип размером не менее 16 бит, работа с которым наиболее быстрая на данной платформе. Типы без приставок обозначают типы с фиксированном размером, равным N бит.

Типы с приставками least- и fast- можно считать заменой типам int, short, long, с той лишь разницей, что первые дают программисту выбрать между скоростью и размером.

Основные типы данных для хранения целых чисел
Тип данных Размер Минимальный диапазон значений Стандарт
signed char минимум 8 бит от −127[39] (= -(27−1)) до 127 C90[j]
int_least8_t C99
int_fast8_t
unsigned char минимум 8 бит от 0 до 255 (=28−1) C90[j]
uint_least8_t C99
uint_fast8_t
char минимум 8 бит от −127 до 127 или от 0 до 255 в зависимости от компилятора C90[j]
short int минимум 16 бит от −32,767 (= -(215−1)) до 32,767 C90[j]
int
int_least16_t C99
int_fast16_t
unsigned short int минимум 16 бит от 0 до 65,535 (= 216−1) C90[j]
unsigned int
uint_least16_t C99
uint_fast16_t
long int минимум 32 бита от −2,147,483,647 до 2,147,483,647 C90[j]
int_least32_t C99
int_fast32_t
unsigned long int минимум 32 бита от 0 до 4,294,967,295 (= 232−1) C90[j]
uint_least32_t C99
uint_fast32_t
long long int минимум 64 бита от −9,223,372,036,854,775,807 до 9,223,372,036,854,775,807 C99
int_least64_t
int_fast64_t
unsigned long long int минимум 64 бита от 0 до 18,446,744,073,709,551,615 (= 264−1)
uint_least64_t
uint_fast64_t
int8_t 8 бит от −127 до 127
uint8_t 8 бит от 0 до 255 (=28−1)
int16_t 16 бит от −32,767 до 32,767
uint16_t 16 бит от 0 до 65,535 (= 216−1)
int32_t 32 бита от −2,147,483,647 до 2,147,483,647
uint32_t 32 бита от 0 до 4,294,967,295 (= 232−1)
int64_t 64 бита от −9,223,372,036,854,775,807 до 9,223,372,036,854,775,807
uint64_t 64 бита от 0 до 18,446,744,073,709,551,615 (= 264−1)
В таблице приведён минимальный диапазон значений согласно стандарту языка. Компиляторы языка Си могут расширять диапазон значений.

Вспомогательные целочисленные типы

Также со стандарта C99 добавлены типы intmax_t и uintmax_t, соответствующие самым большим знаковому и беззнаковому типам соответственно. Данные типы удобны при использовании в макросах для хранения промежуточных или временных значений при операциях над целочисленными аргументами, так как позволяют уместить значения любого типа. Например, эти типы используются в макросах сравнения целочисленных значений библиотеки модульного тестирования Check для языка Си[40].

В Си существует несколько дополнительных целочисленных типов для безопасной работы с типом данных указателей: intptr_t, uintptr_t и ptrdiff_t. Типы intptr_t и uintptr_t из стандарта C99 предназначены для хранения соответственно знакового и беззнакового значений, которые по размеру могут уместить в себе указатель. Эти типы часто применяются для хранения произвольного целого числа в указателе, например, как способ избавиться от лишнего выделения памяти при регистрации функций обратной связи[41] либо при использовании сторонних связных списков, ассоциативных массивов и прочих структур, в которых данные хранятся по указателю. Тип ptrdiff_t из заголовочного файла stddef.h предназначен для безопасного хранения разности двух указателей.

Для хранения размера предусмотрен беззнаковый тип size_t из заголовочного файла stddef.h. Данный тип способен уместить максимально возможное количество байт, доступное по указателю, и обычно используется для хранения размера в байтах. Значение именно этого типа возвращает оператор sizeof[42].

Приведение целочисленных типов

Преобразования целочисленных типов могут происходить как явно, с помощью оператора приведения типов, так и неявно. Значения типов, меньших по размеру, чем int, при участии в каких-либо операциях или при передаче в вызов функции автоматически приводятся к типу int, а в случае невозможности преобразования — к типу unsigned int. Зачастую подобные неявные приведения необходимы, чтобы результат вычисления оказался правильным, но иногда приводят к интуитивно-непонятным ошибкам в вычислениях. Например, если в операции участвуют числа типа int и unsigned int, а знаковое значение отрицательно, то преобразование отрицательного числа к беззнаковому типу приведёт к переполнению и возникновению очень большого положительного значения, что может привести к неверному результату операций сравнения[43].

Сравнение правильного и ошибочного автоматического приведения типов
Знаковый и беззнаковый типы меньше, чем int Знаковый меньше беззнакового, а беззнаковый не менее int
#include <stdio.h>

signed char x = -1;
unsigned char y = 0;
if (x > y) { // условие ложно
    printf("Сообщение не будет показано.\n");
}
if (x == UCHAR_MAX) {
    // условие ложно
    printf("Сообщение не будет показано.\n");
}
#include <stdio.h>

signed char x = -1;
unsigned int y = 0;
if (x > y) { // условие истинно
    printf("Переполнение в переменной x.\n");
}
if ((x == UINT_MAX) && (x == ULONG_MAX)) {
    // условие всегда будет истинным
    printf("Переполнение в переменной x.\n");
}
В данном примере оба типа, знаковый и беззнаковый, будут приведены к знаковому типу int, поскольку он позволяет уместить диапазоны обоих типов. Поэтому сравнение в условном операторе будет корректным. Знаковый тип будет приведён к беззнаковому, поскольку беззнаковый больше или равен по размеру типу int, но произойдёт переполнение, поскольку в беззнаковом типе невозможно представить отрицательное значение.

Также автоматическое приведение типов сработает, если в выражении используется два или более разных целочисленных типа. Стандарт определяет ряд правил, согласно которым выбирается такое преобразование типов, которое может дать правильный результат вычислений. Разным типам назначены разные ранги в рамках преобразования, а сами ранги основаны на размере типа. При участии в выражении разных типов обычно выбирается приведение этих значений к типу большего ранга[43].

Вещественные числа

Числа с плавающей запятой в языке Си представлены тремя основными типами: float, double и long double.

Вещественные числа имеют представление, сильно отличающее их от целых. Константы вещественных чисел разных типов, записанные в десятичном представлении, могут быть не равны друг другу. Например, условие 0.1 == 0.1f будет ложным из-за потери точности у типа float, в то время как условие 0.5 == 0.5f будет истинным, поскольку эти числа конечны в двоичном представлении. Однако условие с приведением (float) 0.1 == 0.1f также будет истинным, поскольку при приведении типа к менее точному теряются разряды, из-за которых в этих двух константах есть различия.

Арифметические операции с вещественными числами также являются неточными и зачастую имеют некоторую плавающую погрешность[44]. Наибольшая погрешность будет возникать при операциях над значениями, близкими к минимально возможному для конкретного типа. Также погрешность может оказаться большой при вычислениях над одновременно очень маленькими (≪ 1) и очень большими по модулю числами (≫ 1). В ряде случаев погрешность может быть снижена изменением алгоритмов и методик вычислений. Например, при замене многократного сложения умножением погрешность может снизиться во столько раз, сколько изначально было операций сложения.

Также в заголовочном файле math.h присутствуют два дополнительных типа float_t и double_t, которые соответствуют как минимум типам float и double соответственно, но могут быть отличными от них. Типы float_t и double_t добавлены в стандарте C99, а их соответствие основным типам определяется значением макроса FLT_EVAL_METHOD.

Вещественные типы данных
Тип данных Размер Стандарт
float 32 бита IEC 60559 (IEEE 754), расширение F стандарта Си[45][k], число одинарной точности
double 64 бита IEC 60559 (IEEE 754), расширение F стандарта Си[45][k], число двойной точности
long double минимум 64 бита зависит от реализации
float_t (C99) минимум 32 бита зависит от базового типа
double_t (C99) минимум 64 бита зависит от базового типа
Соответствие дополнительных типов базовым[46]
FLT_EVAL_METHOD float_t double_t
1 float double
2 double double
3 long double long double

Строки

Нуль-терминированные строки

Хотя как такового специального типа для строк в Си не предусмотрено, в языке активно используются нуль-терминированные строки. ASCII-строки объявляются как массив типа char, последним элементом которого должен быть символ с кодом 0 ('\0'). В этом же формате принято хранить и строки в формате UTF-8. Однако все функции, работающие с ASCII-строками, рассматривают каждый символ как байт, что ограничивает применение стандартных функций при использовании данной кодировки.

Несмотря на широкое распространение идеи нуль-терминированных строк и удобство их использования в некоторых алгоритмах, у них есть несколько серьёзных недостатков.

  1. Необходимость добавления в конец строки терминального символа не даёт возможность получить подстроку без необходимости её копирования, а функций для работы с указателем на подстроку и её длиной в языке не предусмотрено.
  2. Если требуется заранее выделять память под результат алгоритма на основе входных данных, каждый раз требуется обходить всю строку для подсчёта её длины.
  3. При работе с большими объёмами текста подсчёт длины может оказаться узким местом.
  4. Работа со строкой, которая по ошибке не терминирована нулём, может приводить к неопределённому поведению программы, в том числе к ошибкам сегментирования, ошибкам переполнения буфера и к уязвимостям.

В современных условиях, когда производительность кода приоритетнее расхода памяти, может оказаться эффективнее и проще использовать структуры, содержащие в себе как саму строку, так и её размер[47][⇨], например:

struct string_t {
    char *str; // указатель на строку
    size_t str_size; // размер строки
};
typedef struct string_t string_t; // альтернативное имя для упрощения кода

Альтернативным вариантом хранения размера строки с низким потреблением памяти может оказаться подход добавления в начало строки её размера в формате размера переменной длины  (англ.). Подобный подход применяется в протокольных буферах, однако только на этапе передачи данных, но не их хранения.

Строковые литералы

Строковые литералы в Си по своей сути являются константами[9]. При объявлении заключаются в двойные кавычки, а терминирующий 0 добавляются компилятором автоматически. Допускается два способа присваивания строкового литерала: по указателю и по значению. При присваивании по указателю в переменную типа char * заносится указатель на неизменяемую строку, то есть формируется константная строка. Если же заносить строковый литерал в массив, то происходит копирование строки в область стека.

#include <stdio.h>
#include <string.h>

int main(void)
{
    const char *s1 = "Константная строка";
    char s2[] = "Строка, которую можно менять";
    memcpy(s2, "с", strlen("с")); // замена первой буквы на маленькую
    puts(s2); // выведется текст строки
    memcpy((char *) s1, "к", strlen("к")); // ошибка сегментирования
    puts(s1); // строка не будет исполнена
}

Поскольку строки являются обычными массивами символов, вместо литералов можно использовать инициализаторы, если каждый символ умещается в 1 байт:

char s[] = {'I', 'n', 'i', 't', 'i', 'a', 'l', 'i', 'z', 'e', 'r', '\0'};

Однако на практике такой подход имеет смысл только в крайне редких случаях, когда к ASCII-строке требуется не добавлять терминирующий ноль.

Широкие строки

Кодировка типа wchar_t в зависимости от платформы
Платформа Кодировка
GNU/Linux USC-4[48]
macOS
Windows USC-2[49]
AIX
FreeBSD Зависит от локали,

не документировано[49]

Solaris

Альтернативой обычным строкам могут служить широкие строки, в которых каждый символ хранится в специальном типе wchar_t. Данный тип по стандарту должен быть способен уместить в себе все символы самой большой из существующих локалей. Функции для работы с широкими строками описаны в заголовочном файле wchar.h, а функции для работы с широкими символами описаны в заголовочном файле wctype.h.

При объявлении строковых литералов для широких строк используется модификатор L:

const wchar_t *wide_str = L"Широкая строка";

В форматированном выводе используется спецификатор %ls, однако спецификатор размера, если задан, указывается в байтах, а не в символах[50].

Тип wchar_t задумывался для того, чтобы в него мог поместиться любой символ, а широкие строки — для хранения строк любой локали, но в результате API оказался неудобным, а реализации — платформозависимыми. Так, на платформе Windows в качестве размера типа wchar_t было выбрано 16 бит, а позже появился стандарт UTF-32, таким образом тип wchar_t на платформе Windows уже не способен уместить в себе все символы из кодировки UTF-32, в результате чего теряется смысл данного типа[49]. В то же время на платформах Linux[48] и macOS данный тип занимает 32 бита, поэтому для реализации кроссплатформенных задач тип wchar_t не подходит.

Многобайтовые строки

Существует много разных кодировок, в которых отдельный символ может быть запрограммирован разным количеством байт. Такие кодировки называются многобайтовыми. К ним относится также и UTF-8. В Си существует набор функций для преобразования строк из многобайтовых в рамках текущей локали в широкие и наоборот. Функции для работы с многобайтовыми символами имеют префикс либо суффикс mb и описаны в заголовочном файле stdlib.h. Для поддержки многобайтовых строк в программах на языке Си, такие строки должны поддерживаться на уровне текущей локали. Для явного задания кодировки можно менять текущую локаль с помощью функции setlocale() из заголовочного файла locale.h. Однако задание кодировки для локали должно поддерживаться используемой стандартной библиотекой. Так, например, стандартная библиотека Glibc полностью поддерживает кодировку UTF-8 и способна преобразовывать текст во множество других кодировок[51].

Начиная со стандарта C11 язык поддерживает также 16-битные и 32-битные широкие многобайтовые строки с соответствующими типами символа char16_t и char32_t из заголовочного файла uchar.h, а также объявление строковых литералов в формате UTF-8 с помощью модификатора u8. 16-битные и 32-битные строки могут использоваться для хранения кодировок UTF-16 и UTF-32, если в заголовочном файле uchar.h заданы макроопределения __STDC_UTF_16__ и __STDC_UTF_32__, соответственно. Для задания строковых литералов в данных форматах используются модификаторы: u для 16-битных строк и U для 32-битных строк. Примеры объявления строковых литералов для многобайтовых строк:

const char *s8 = u8"Многобайтовая строка в кодировке UTF-8";
const char16_t *s16 = u"16-битная многобайтовая строка";
const char32_t *s32 = U"32-битная многобайтовая строка";

Следует иметь в виду, что функция c16rtomb() для преобразования из 16-битной строки в многобайтовую работает не так, как задумывалось, и в стандарте C11 оказалась неспособной переводить из UTF-16 в UTF-8[52]. Исправление работы данной функции может зависеть от конкретной реализации компилятора.

Пользовательские типы

Перечисления

Перечисления представляют собой набор именованных целочисленных констант и обозначаются с помощью ключевого слова enum. Если константе не сопоставлено число, то ей автоматически задаётся либо 0 для первой константы в списке, либо число на единицу бо́льшее, чем задано в предыдущей константе. При этом сам тип данных перечисления по факту может соответствовать любому знаковому или беззнаковому примитивному типу, в диапазон которого умещаются все значения перечислений; решение о выборе того или иного типа принимает компилятор. Однако явно заданные значения для констант должны быть выражениями типа int[17].

Тип перечисления может быть также анонимным, если не указано название перечисления. Константы, указанные в двух разных перечислениях, относятся к двум разным типам данных, независимо от того, являются ли перечисления именованными или анонимными.

На практике перечисления часто используются для обозначения состояний конечных автоматов, для задания вариантов режимов работы или значений параметров[53], для создания целочисленных констант, а также для перечисления каких-либо уникальных объектов или свойств[54].

Структуры

Структуры представляют собой объединение переменных разных типов данных в рамках одной области памяти; обозначаются ключевым словом struct. Переменные внутри структуры называются полями структуры. С точки зрения адресного пространства поля всегда идут друг за другом в том же порядке, в котором указаны, но компиляторы могут выравнивать адреса полей для оптимизации под ту или иную архитектуру. Таким образом, фактически поле может занимать бо́льший размер, чем указано в программе.

Каждое поле имеет определённое смещение относительно адреса структуры и размер. Смещение можно получить с помощью макроса offsetof() из заголовочного файла stddef.h. При этом смещение будет зависеть от выравнивания и размера предыдущих полей. Размер поля обычно определяется выравниванием структуры: если размер выравнивания типа данных поля меньше значения выравнивания структуры, то размер поля определяется выравниванием структуры. Выравнивание типов данных можно получить с помощью макроса alignof()[f] из заголовочного файла stdalign.h. Размер самой структуры является совокупным размером всех её полей с учётом выравнивания. При этом некоторые компиляторы предоставляют специальные атрибуты, позволяющие упаковывать структуры, убирая из них выравнивания[55].

Полям структур можно явно задавать размер в битах через двоеточие после определения поля и указание количества бит, что ограничивает диапазон их возможных значений, несмотря на тип поля. Подобный подход может использоваться как альтернатива флагам и битовым маскам для обращения к ним. Однако указание количества бит не отменяет возможного выравнивания полей структур в памяти. Работа с битовыми полями имеет ряд ограничений: к ним невозможно применить оператор sizeof или макрос alignof(), на них невозможно получить указатель.

Объединения

Объединения необходимы в тех случаях, когда требуется обращаться к одной и той же переменной как к разным типам данных; обозначаются ключевым словом union. Внутри объединения может быть объявлено произвольное количество пересекающихся полей, которые по факту предоставляют доступ к одной и той же области памяти как к разным типам данных. Размер объединения выбирается компилятором исходя из размера самого большого поля в объединении. Следует иметь в виду, что изменение одного поля объединения приводит к изменению и всех других полей, но гарантированно правильным будет только значение того поля, которое менялось.

Объединения могут служить более удобной альтернативной приведению указателя к произвольному типу. К примеру, с помощью объединения, помещённого в структуру, можно создавать объекты с динамически меняющимся типом данных:

Массивы

Массивы в языке Си примитивны и являются лишь синтаксическим абстрагированием над арифметикой указателей. Сам по себе массив является указателем на область памяти, поэтому вся информация о размерности массива и его границах может быть доступна только на этапе компиляции согласно описанию типа. Массивы могут быть как одномерными, так и многомерными, но обращение к элементу массива сводится к простому вычислению смещения относительно адреса начала массива. Поскольку массивы основаны на адресной арифметике, с ними возможно работать без использования индексов[56]. Так, например, следующие два примера считывания с потока ввода 10 чисел идентичны друг другу:

Длина массивов с заранее известным размером вычисляется на этапе компиляции. В стандарте C99 появилась возможность объявлять массивы переменной длины, у которых длина может задаваться на этапе выполнения. Под такие массивы выделяется память из области стека, поэтому их необходимо использовать с осторожностью, если их размер может задаваться извне программы. В отличие от выделения динамической памяти, превышение допустимого размера в области стека может повлечь непредсказуемые последствия, а отрицательная длина массива — неопределённое поведение. Начиная с C11 массивы переменной длины являются опциональными для компиляторов, а отсутствие поддержки определяется наличием макроса __STDC_NO_VLA__[57].

Массивы фиксированного размера, объявляемые как локальные или глобальные переменные, можно инициализировать, задавая им начальное значение с помощью фигурных скобок и перечисления элементов массива через запятую. В инициализаторах глобальных массивов допускается использовать только такие выражения, которые вычисляются на этапе компиляции[58]. Переменные, используемые в таких выражениях должны объявляться как константы, с модификатором const. У локальных массивов инициализаторы могут содержать выражения с вызовами функций и использованием других переменных, в том числе можно заносить указатель на сам объявляемый массив.

Со стандарта C99 последним элементом структур допускается объявлять массив произвольной длины, что широко используется на практике и поддерживается различными компиляторами. Размер такого массива зависит от объёма памяти, выделяемого под структуру. При этом нельзя объявлять массив таких структур и нельзя их помещать в другие структуры. В операциях над такой структурой массив произвольной длины обычно игнорируется, в том числе и при вычислении размера структуры, а выход за пределы массива влечёт за собой неопределённое поведение[59].

Язык Си не предусматривает какого-либо контроля выхода за пределы массива, поэтому программист сам должен следить за работой с массивами. Ошибки при обработке массивов не всегда явно влияют на ход исполнения программы, но могут приводить к ошибкам сегментирования и уязвимостям[⇨].

Синонимы типов

Язык Си допускает создание собственных названий типов с помощью оператора typedef. Альтернативные названия можно задавать как системным типам, так и пользовательским. Такие названия объявляются в глобальном пространстве имён и не конфликтуют с названиями типов структур, перечислений и объединений.

Альтернативные названия могут применяться как для упрощения кода, так и для создания уровней абстракции. Например, некоторые системные типы можно сократить для повышения читабельности кода или унифицирования его написания в пользовательском коде:

#include <stdint.h>

typedef int32_t i32_t;
typedef int_fast32_t i32fast_t;
typedef int_least32_t i32least_t;

typedef uint32_t u32_t;
typedef uint_fast32_t u32fast_t;
typedef uint_least32_t u32least_t;

Примером абстрагирования могут служить названия типов в заголовочных файлах операционных системах. Так, стандарт POSIX определяет тип pid_t, предназначенный для хранения числового идентификатора процесса. На самом деле данный тип является альтернативным названием для какого-либо примитивного типа, например:

typedef int             __kernel_pid_t;
typedef __kernel_pid_t  __pid_t
typedef __pid_t         pid_t;

Поскольку типы с альтернативными названиями являются лишь синонимами оригинальным типам, то между ними сохраняется полная совместимость и взаимозаменяемость.

Препроцессор

Препроцессор работает до компиляции и преобразует текст файла программы согласно встреченным в нём или переданным в препроцессор директивам. Технически препроцессор может быть реализован по-разному, но логически его удобно представлять именно как отдельный модуль, целиком обрабатывающий каждый предназначенный для компиляции файл и формирующий текст, попадающий затем на вход компилятора. Препроцессор ищет в тексте строки, начинающиеся с символа #, вслед за которым должны следовать директивы препроцессора. Всё, что не относится к директивам препроцессора и не исключено из компиляции согласно директивам, передаётся на вход компилятора в неизменном виде.

В число возможностей препроцессора входит:

  • подмена заданной лексемы текстом с помощью директивы #define, включая возможность создания параметризованных шаблонов текста (вызываются аналогично функциям), а также отменять подобные подмены, что даёт возможность осуществлять подмену на ограниченных участках текста программы;
  • условное встраивание и удаление кусков из текста, включая сами директивы, с помощью условных команд #ifdef, #ifndef, #if, #else и #endif;
  • встраивание в текущий файл текста из другого файла с помощью директивы #include.

Важно понимать, что препроцессор обеспечивает только подстановку текста, не учитывая синтаксис и семантику языка. Так, например, макроопределения #define могут встречаться внутри функций или определений типов, а директивы условной компиляции могут приводить к исключению из компилируемого текста программы любой части кода, без оглядки на грамматику языка. Вызов параметрического макроса также отличается от вызова функции, поскольку не происходит анализа семантики аргументов, разделяемых запятой. Так, например, в аргументы параметрического макроса невозможно передать инициализацию массива, поскольку его элементы тоже разделяются запятой:

#define array_of(type, array) (((type) []) (array))
int *a;
a = array_of(int, {1, 2, 3}); // ошибка компиляции:
// в макрос "array_of" передано 4 аргумента, но он принимает лишь 2

Макроопределения часто используются для обеспечения совместимости с разными версиями библиотек, у которых изменился API, включая те или иные участки кода в зависимости от версии библиотеки. Для этих целей библиотеки часто предоставляют макроопределения с описанием своей версии[60], а иногда и макросы с параметрами для сравнения текущей версии с заданной в рамках препроцессора[61]. Также макроопределения применяются для условной компиляции отдельных кусков программы, например для включения поддержки какого-либо дополнительного функционала.

Макроопределения с параметрами широко используются в Си-программах для создания аналогов обобщённых функций. Ранее они также применялись для реализации встраиваемых функций, но начиная со стандарта С99 эта необходимость исчезла благодаря добавлению inline-функций. Однако в связи с тем, что макроопределения с параметрами функциями не являются, но вызываются аналогичным образом, по ошибке программиста могут возникать неожиданные проблемы, включая отработку только части кода из макроопределения[62] и неправильные приоритеты выполнения операций[63]. В качестве примера ошибочного кода можно привести макрос возведения числа в квадрат:

#include <stdio.h>

int main(void)
{
    #define SQR(x) x * x
    printf("%d", SQR(5)); // всё верно, 5*5=25
    printf("%d", SQR(5 + 0)); // предполагалось 25, но будет выведено 5 (5+0*5+0)
    printf("%d", SQR(4 / 3)); // всё верно, 1 (т. к. 4/3=1, 1*4=4, 4/3=1)
    printf("%d", SQR(5 / 2)); // предполагалось 4 (2*2), но будет выведено 5 (5/2*5/2)
    return 0;
}

В приведённом выше примере ошибкой является то, что содержимое аргумента макроса подставляется в текст как есть, без учёта приоритетов операций. В таких случаях необходимо использовать inline-функции либо явно расставлять приоритеты операций в выражениях, использующих параметры макроса, с помощью круглых скобок:

#include <stdio.h>

int main(void)
{
    #define SQR(x) ((x) * (x))
    printf("%d", SQR(4 + 1)); // верно, 25
    return 0;
}

Программирование на Си

Структура программы

Модули

Программа представляет собой набор файлов с кодом на языке Си, которые могут компилироваться в объектные файлы. Объектные файлы затем проходят этап компоновки друг с другом, а также со внешними библиотеками, в результате чего получается итоговый исполняемый файл или библиотека. Связь файлов друг с другом, равно как и с библиотеками, требует описания прототипов используемых функций, внешних переменных и необходимых типов данных в каждом файле. Такие данные принято выносить в отдельные заголовочные файлы, которые подключаются с помощью директивы #include в тех файлах, где требуется та или иная функциональность, и позволяют организовывать систему, похожую на систему модулей. Модулем в таком случае может выступать:

  • набор отдельных файлов с исходным кодом, для которых представлен интерфейс в виде заголовочных файлов;
  • объектная библиотека или её часть, с соответствующими заголовочными файлами;
  • самодостаточный набор из одного или более заголовочных файлов (интерфейсная библиотека);
  • статическая библиотека или её часть с соответствующими заголовочными файлами;
  • динамическая библиотека или её часть с соответствующими заголовочными файлами.

Поскольку директива #include лишь подставляет текст другого файла на этапе препроцессора, многократное подключение одного и того же файла может приводить к ошибкам этапа компиляции. Поэтому в таких файлах используется защита от повторного включения с помощью макрокоманд #define и #ifndef[64].

Файлы исходного кода

Текст файла исходного кода на языке Си состоит из набора глобальных определений данных, типов и функций. Глобальные переменные и функции, объявленные со спецификаторами static и inline, доступны только в пределах того файла, в котором они объявлены, либо при включении одного файла в другой через директиву #include. При этом функции и переменные, объявленные в заголовочном файле со словом static, будут создаваться заново при каждом подключении заголовочного файла к очередному файлу с исходным кодом. Глобальные переменные и прототипы функции, объявленные со спецификатором extern, считаются подключаемыми из других файлов. То есть их допускается использовать в соответствии с описанием; предполагается, что после сборки программы они будут связаны компоновщиком с оригинальными объектами и функциями, описанными в своих файлах.

Глобальные переменные и функции, кроме static и inline, могут быть доступны из других файлов при условии их надлежащего объявления там со спецификатором extern. Переменные и функции, объявленные с модификатором static, также могут быть доступны в других файлах, но лишь при передаче их адреса по указателю. Объявления типов typedef, struct и union не могут импортироваться в других файлах. При необходимости использования в других файлах они должны быть там продублированы либо вынесены в отдельный заголовочный файл. То же самое относится и к inline-функциям.

Точка входа программы

Для исполняемой программы стандартной точкой входа является функция с именем main, которая не может быть статической и должна быть единственной в программе. Исполнение программы начинается с первого оператора функции main() и продолжается до выхода из неё, после чего программа завершается и возвращает операционной системе абстрактный целочисленный код результата своей работы.

Допустимые прототипы функции main()[65]
Без аргументов С аргументами командной строки
int main(void);
int main(int argc, char** argv);

В переменную argc при вызове передаётся количество аргументов, переданных программе, включая и путь к самой программе, поэтому обычно переменная argc содержит значение не меньшее, чем 1. В переменную argv передаётся сама строка запуска программы в виде массива текстовых строк, последним элементом которого является NULL. Компилятор гарантирует, что на момент запуска функции main() все глобальные переменные в программе будут инициализированы[66].

В качестве результата функция main() может вернуть любое целое число в диапазоне значений типа int, которое будет передано операционной системе или другому окружению в качестве кода возврата программы[65]. Стандарт языка не определяет смысла кодов возврата[67]. Обычно операционная система, где работают программы, имеет те или иные средства, позволяющие получить значение кода возврата и проанализировать его. Иногда существуют определённые соглашения о значениях этих кодов. Общим является соглашение о том, что нулевое значение кода возврата сигнализирует об успешном завершении программы, а ненулевое представляет собой код возникшей ошибки. Заголовочный файл stdlib.h определяет два общих макроопределения EXIT_SUCCESS и EXIT_FAILURE, которые соответствуют успешному и неуспешному завершению работы программы[67]. Коды возврата также могут использоваться в рамках приложений, включающих в себя множество процессов, для обеспечения взаимодействия между этими процессами, в случае чего приложение само определяет смысловое значение для каждого кода возврата.

Работа с памятью

Модель памяти

В Си предусмотрено 4 способа выделения памяти, которые определяют время жизни переменной и момент её инициализации[66].

Способы выделения памяти[66]
Способ выделения Целевые объекты Время выделения Время освобождения Накладные расходы
Статическое выделение памяти Глобальные переменные и переменные, помеченные ключевым словом static (но без _Thread_local) При старте программы По завершении работы программы Отсутствуют
Выделение памяти на уровне потока Переменные, помеченные ключевым словом _Thread_local При старте потока По завершении потока При создании потока
Автоматическое выделение памяти Аргументы функций и возвращаемые ими значения, локальные переменные функций, в том числе регистры и массивы переменной длины При вызове функций на уровне стека. Автоматически по завершении функций Незначительны, поскольку изменяется лишь указатель на вершину стека
Динамическое выделение памяти Память, выделяемая через функции malloc(), calloc() и realloc() Вручную из кучи в момент вызова используемой функции. Вручную с помощью функции free() Большие как на выделение, так и на освобождение

Все эти способы хранения данных пригодны в различных ситуациях и имеют свои преимущества и недостатки. Глобальные переменные не позволяют писать реентерабельные алгоритмы, а автоматическое выделение памяти не позволяет возвращать произвольную область памяти из вызова функции. Автоматическое выделение также не подходит для выделения больших объёмов памяти, поскольку может привести к порче стека или кучи[68]. Динамическая память лишена этих недостатков, но имеет большие накладные расходы при её использовании и более сложна в использовании.

Там, где это возможно, предпочтительным является автоматическое или статическое выделение памяти: такой способ хранения объектов управляется компилятором, что освобождает программиста от трудностей ручного выделения и освобождения памяти, как правило, служащего источником трудно отыскиваемых ошибок утечек памяти, ошибок сегментирования и повторного освобождения в программе. К сожалению, многие структуры данных имеют переменный размер во время выполнения программы, поэтому из-за того, что автоматически и статически выделенные области должны иметь известный фиксированный размер во время компиляции, очень часто требуется использовать динамическое выделение.

Для автоматически выделяемых переменных с помощью модификатора register можно давать подсказку компилятору о необходимости быстрого доступа к ним. Такие переменные могут помещаться в регистры процессора. Из-за ограниченного количества регистров и возможных оптимизаций компилятора переменные могут оказаться в обычной памяти, но тем не менее из программы на них невозможно будет получить указатель[69]. Модификатор register является единственным, который можно указывать в аргументах функций[70].

Адресация памяти

Язык Си унаследовал линейную адресацию памяти при работе со структурами, массивами и выделенными областями памяти. Стандарт языка также допускает выполнение операций сравнения над нулевым указателем и над адресами в рамках массивов, структур и выделенных областей памяти. Также допускается работа с адресом элемента массива, следующим за последним, что сделано для облегчения написания алгоритмов. Однако сравнение указателей адресов, полученных для разных переменных (или областей памяти) не должно осуществляться, так как результат будет зависеть от реализации конкретного компилятора[71].

Представление в памяти

Представление памяти программы зависит от аппаратной архитектуры, от операционной системы и от компилятора. Так, например, на большинстве архитектур стек растёт вниз, но существуют архитектуры, где стек растёт вверх[72]. Граница между стеком и кучей может быть частично защищена от переполнения стека специальной областью памяти[73]. А расположение данных и кода библиотек может зависеть от параметров компиляции[74]. Стандарт Си абстрагируется над реализацией и позволяет писать переносимый код, однако понимание устройства памяти процесса помогает в отладке и написании безопасных и отказоустойчивых приложений.

Типовое представление памяти процесса в Unix-подобных ОС
Типовое представление виртуальной памяти процесса в Unix-подобных ОС[75]

При запуске программы из исполняемого файла в оперативную память импортируются инструкции процессора (машинный код) и инициализированные данные. В то же время в старшие адреса импортируются аргументы командной строки (доступные в функции main() со следующей сигнатурой во втором аргументе int argc, char ** argv) и переменные окружения.

Область неинициализированных данных содержит глобальные переменные (в том числе, объявленные как static), которые не были проинициализированы в программном коде. Такие переменные по умолчанию инициализируются нулями после старта программы. Область инициализированных данных — сегмент данных — тоже содержит глобальные переменные, но в эту область попадают те переменные, которым было задано начальное значение. Неизменяемые данные, включающие в себя переменные, объявленные с модификатором const, строковые литералы и другие составные литералы, помещаются в сегмент текста программы. Сегмент текста программы содержит также исполняемый код и доступен только на чтение, поэтому попытка изменения данных из этого сегмента приведёт к неопределённому поведению в виде ошибки сегментации.

Область стека предназначена для размещения данных, связанных с вызовом функций, и локальных переменных. Перед каждым запуском функции стек увеличивается для размещения в нём аргументов, передаваемых в функцию. В ходе своей работы функция может размещать в стеке локальные переменные и выделять в нём память под массивы переменной длины, а некоторые компиляторы предоставляют также средства выделения памяти в рамках стека посредством вызова alloca(), который не входит в стандарт языка. После завершения работы функции стек уменьшается до того значения, которое было перед вызовом, однако этого может не происходить при некорректной работе со стеком. Память, выделенная динамически, предоставляется из кучи[⇦].

Немаловажной деталью является наличие случайного отступа между стеком и верхней областью[76], а также между областью инициализированных данных и кучей. Делается это в целях безопасности, например, для предотвращения встраивания в стек других функций.

Динамически подключаемые библиотеки и отображения файлов с файловой системы находятся между стеком и кучей[77].

Обработка ошибок

В Си отсутствуют какие-либо встроенные механизмы контроля ошибок, но существует несколько общепринятых способов их обработки средствами языка. В общем виде практика обработки ошибок языка Си в отказоустойчивом коде вынуждает писать громоздкие, часто повторяющиеся конструкции, в которых алгоритм совмещён с обработкой ошибок[⇨].

Маркеры ошибок и errno

В языке Си активно используется специальная переменная errno из заголовочного файла errno.h, в которую функции заносят код ошибки, возвращая при этом значение, являющееся маркером ошибки. Для проверки результата на ошибки результат сравнивают с маркером ошибки, и, если они совпадают, то можно проанализировать код ошибки, сохранённый в errno, для корректировки работы программы или вывода отладочного сообщения. В стандартной библиотеке стандарт зачастую лишь определяет возвращаемые маркеры ошибок, а выставление errno зависит от конкретной реализации[78].

В качестве маркеров ошибок обычно выступают следующие значения:

  • -1 для типа int в случаях, когда отрицательный диапазон результата не используется[79];
  • -1 для типа ssize_t (POSIX)[80];
  • (size_t) -1 для типа size_t[79];
  • (time_t) -1 при использовании некоторых функций для работы со временем[79];
  • NULL для указателей[79];
  • EOF при потоковой работе с файлами[79];
  • ненулевой код ошибки[79].

Практика возвращения маркера ошибки, вместо кода ошибки, хоть и экономит количество передаваемых в функции аргументов, но в ряде случаев приводит к ошибкам в результате человеческого фактора. Например, программистами часто игнорируется проверка результата типа ssize_t, а сам результат используется дальше в вычислениях, что приводит к трудно уловимым ошибкам, если возвращается -1[81].

Ещё сильнее способствует появлению ошибок возврат в качестве маркера ошибки корректного значения[81], что также вынуждает программиста делать больше проверок, а соответственно и писать больше однотипного повторяющегося кода. Такой подход практикуется в потоковых функциях, работающих с объектами типа FILE *: маркером ошибки является значение EOF, одновременно являясь и маркером конца файла. Поэтому по EOF иногда приходится проверять поток символов как на конец файла с помощью функции feof(), так и наличие ошибки с помощью ferror()[82]. При этом некоторые функции, которые могут вернуть EOF по стандарту не обязаны выставлять errno[78][⇨].

Отсутствие единой практики обработки ошибок в стандартной библиотеке приводит к появлению собственных способов обработки ошибок и комбинированию часто используемых способов в сторонних проектах. Например, в проекте systemd совместили идеи возвращения кода ошибки и числа -1 в качестве маркера — возвращается отрицательный код ошибки[83]. А в библиотеке GLib ввели в практику возвращение в качестве маркера ошибки значение булева типа, в то время как подробная информация об ошибке помещается в специальную структуру, указатель на которую возвращается через последний аргумент функции[84]. Схожее решение использует проект Enlightenment, в котором в качестве маркера тоже используется булев тип, но информация об ошибке возвращается по аналогии со стандартной библиотекой — через отдельную функцию[85], которую необходимо проверять, если был возвращён маркер.

Возврат кода ошибки

Альтернативой маркерам ошибок является возвращение кода ошибки напрямую, а результата работы функции — через аргументы по указателю. По такому пути пошли разработчики стандарта POSIX, в функциях которого принято возвращать код ошибки в виде числа типа int. Однако возвращение значения типа int явно не даёт понять, что возвращается именно код ошибки, а не маркер, что может вести к ошибкам, если результат таких функций будет проверяться на значение -1. В расширении K стандарта C11 представлен специальный тип errno_t для хранения кода ошибки. Существуют рекомендации использовать именно этот тип в пользовательском коде для возвращения ошибок, а если он не предоставлен стандартной библиотекой, то объявлять его самостоятельно[86]:

#ifndef __STDC_LIB_EXT1__
  typedef int errno_t;
#endif

Такой подход, помимо повышения качества кода, избавляет от необходимости использования errno, что позволяет делать библиотеки с реентерабельными функциями без необходимости подключения дополнительных библиотек, таких как POSIX Threads для правильного определения errno.

Ошибки в математических функциях

Более сложной является обработка ошибок в математических функциях из заголовочного файла math.h, в которых могут возникать 3 типа ошибок[87]:

  • выход за пределы диапазона входных значений;
  • получение бесконечного результата для конечных входных данных;
  • выход результата за пределы диапазона используемого типа данных.

Предотвращение двух из трёх типов ошибок сводится к проверкам входных данных на область допустимых значений. Однако предсказать выход результата за пределы типа крайне сложно. Поэтому стандартом языка предусмотрена возможность анализа математических функций на ошибки. Начиная со стандарта C99 такой анализ возможен двумя способами, в зависимости от значения, хранимого в макросе math_errhandling.

  1. Если выставлен бит MATH_ERRNO, то переменную errno необходимо предварительно сбросить в 0, а после вызова математической функции — проверить на ошибки EDOM и ERANGE.
  2. Если выставлен бит MATH_ERREXCEPT, то возможные математические ошибки предварительно сбрасываются функцией feclearexcept() из заголовочного файла fenv.h, а после вызова математической функции —тестируются с помощью функции fetestexcept().

При этом способ обработки ошибок определяется конкретной реализацией стандартной библиотеки и может отсутствовать совсем. Поэтому в платформонезависимом коде может потребоваться проверка результата сразу двумя способами, в зависимости от значения math_errhandling[87].

Освобождение ресурсов

Как правило возникновение ошибки требует завершения работы функции с возвращением индикатора ошибки. Если в функции ошибка может возникнуть в разных её частях, требуется освобождать ресурсы, выделенные в ходе её работы, чтобы предотвратить утечки. Хорошей практикой освобождения ресурсов считается их чистка в обратном порядке перед возвратом из функции, а в случае ошибок — освобождение в обратном порядке после основного return. В отдельные части такого освобождения можно сделать переход с помощью оператора goto[88]. Подобный подход позволяет вынести не связанные с реализуемым алгоритмом участки кода за пределы самого алгоритма, повышая читабельность кода, и схож с работой оператора defer из языка программирования Go. Пример освобождения ресурсов приведён ниже, в разделе примеров[⇨].

Для освобождения ресурсов в рамках программы предусмотрен механизм обработчиков выхода из программы. Обработчики назначаются с помощью функции atexit() и исполняются как по завершении функции main() через оператор return, так и по исполнению функции exit(). При этом обработчики не исполняются по функциям abort() и _Exit()[89].

В качестве примера освобождения ресурсов по завершении программы можно привести освобождение памяти, выделенной под глобальные переменные. Несмотря на то, что память так или иначе освобождается по завершении работы программы операционной системой, и допускается не освобождать ту память, которая требуется на протяжении всей работы программы[90], явное освобождение предпочтительнее, так как облегчает поиск утечек памяти сторонними средствами и уменьшает шанс на возникновение утечек памяти в результате ошибки:

Недостатком данного подхода является то, что формат назначаемых обработчиков не предусматривает передачу произвольных данных в функцию, что позволяет создавать обработчики только для глобальных переменных.

Примеры программ на Си

Минимальная программа на Си

Минимальная программа на Си, не требующая обработки аргументов, имеет следующий вид:

int main(void){}

Допускается не писать оператор return у функции main(). В таком случае, согласно стандарту, функция main() возвращает 0, исполняя все обработчики, назначенные на функцию exit(). При этом подразумевается, что программа успешно завершилась[39].

Hello, world!

Программа Hello, world! приведена ещё в первом издании книги «Язык программирования Си» Кернигана и Ритчи:

#include <stdio.h>

int main(void) // Не принимает аргументы
{  
    printf("Hello, world!\n"); // '\n' - новая строка
    return 0; // Удачное завершение программы
}

Эта программа печатает сообщение «Hello, world!» на стандартном устройстве вывода.

Обработка ошибок на примере чтения файла

Многие функции языка Си могут вернуть ошибку, не выполнив требуемых от них действий. Ошибки требуется проверять и правильно на них реагировать, в том числе часто требуется пробрасывать ошибку из функции на уровень выше для анализа. При этом функцию, в которой произошла ошибка, можно делать реентерабельной, в таком случае по ошибке функция не должна изменять входные или выходные данные, что позволяет безопасно перезапускать её после исправления ошибочной ситуации.

В примере реализована функция чтения файла на языке Си, однако она требует соответствия функций fopen() и fread() стандарту POSIX, иначе они могут не выставлять переменную errno, что сильно усложняет как отладку, так и написание универсального и безопасного кода. На платформах, не соответствующих POSIX, поведение данной программы будет неопределённым в случае ошибки[⇨]. Освобождение ресурсов по ошибкам находится за основным алгоритмом для повышения читабельности, а переход осуществляется с помощью goto[88].

Средства разработки

Компиляторы

Некоторые компиляторы идут в комплекте с компиляторами других языков программирования (включая C++) или являются составной частью среды разработки программного обеспечения.

  • GNU Compiler Collection (GCC) полностью поддерживает стандарты C99 и C17 (C11 с исправлениями)[91]. Также поддерживает расширения GNU, защиту кода с помощью санитайзеров и набор дополнительных возможностей, в том числе атрибуты.
  • Clang также полностью поддерживает стандарты C99[92] и C17[93]. Разрабатывается во многом совместимым с компилятором GCC, в том числе поддерживает расширения GNU и защиту кода санитайзерами.

Реализации стандартной библиотеки

Несмотря на то, что стандартная библиотека входит в стандарт языка, её реализации идут отдельно от компиляторов. Поэтому стандарты языка, поддерживаемые компилятором и библиотекой, могут различаться.

  • Открытая библиотека glibc является основной во многих дистрибутивах GNU/Linux, поддерживает стандарты C11 и POSIX.1-2008[94], а также предоставляет набор исправлений и дополнительных возможностей от GNU.
  • Открытая библиотека musl задумывалась в качестве более легковесной замены для glibc, используется как библиотека по умолчанию в дистрибутиве Alpine Linux[95], Void Linux[96].
  • Библиотека CRT от Microsoft поддерживает стандарт C99, поставляется как компонент в составе Windows 10[97].

Интегрированные среды разработки

  • CLion полностью поддерживает C99, но поддержка С11 — частичная[98], сборка основана на CMake.
  • Code::Blocks — свободная кроссплатформенная интегрированная среда разработки для языков Си, C++, D, Fortran. Поддерживает более двух десятков компиляторов. С компилятором GCC доступен Си всех версий от C90 до C17.
  • Eclipse — свободная интегрированная среда разработки, поддерживающая язык Си стандарта С99. Имеет модульную архитектуру, что даёт возможность подключения поддержки разных языков программирования и дополнительных возможностей. Доступен модуль для интеграции с Git, однако отсутствует интеграция с CMake.
  • KDevelop — свободная интегрированная среда разработки, поддерживающая некоторые особенности языка Си из стандарта C11. Позволяет управлять проектами, использующими разные языки программирования, включая C++ и Python, поддерживает систему сборки CMake. Имеет встроенную поддержку Git на уровне работы с файлами и настраиваемое форматирование исходного кода для разных языков.
  • Microsoft Visual Studio лишь частично поддерживает стандарты C99 и C11, поскольку ориентируется на разработку под C++, однако имеет встроенную поддержку CMake.

Средства модульного тестирования

Поскольку язык Си не предоставляет средств для безопасного написания кода, а многие элементы языка способствуют появлению ошибок, написание качественного и отказоустойчивого кода можно гарантировать только с помощью создания автоматизированных тестов. Для упрощения такого тестирования существуют различные реализации сторонних библиотек модульного тестирования.

  • Библиотека Check предоставляет фреймворк для тестирования программного кода на языке Си в общепринятом стиле xUnit. Среди возможностей можно упомянуть запуск тестов в отдельных процессах через fork(), что позволяет распознавать в тестах ошибки сегментирования[99], а также даёт возможность задавать максимальное время исполнения отдельных тестов.
  • Библиотека Google Test также предоставляет тестирование по принципам xUnit, но предназначена для тестирования кода на языке C++, что позволяет её использовать для тестирования кода и на языке Си. Также поддерживает изолированное тестирование отдельных частей программы. Одним из достоинств библиотеки является разделение макросов тестирования на утверждения и ошибки, что может облегчить отладку кода.

Существует также много других систем для тестирования кода на Си, таких как AceUnit, GNU Autounit, cUnit и других, но они либо не осуществляют тестирование в изолированных окружениях, либо предоставляют мало возможностей[99], либо перестали развиваться.

Средства отладки

По проявлениям ошибок не всегда можно сделать однозначный вывод о проблемном месте в коде, однако локализовать проблему часто помогают различные средства отладки.

  • Gdb — интерактивный консольный отладчик для различных языков, в том числе и для Си.
  • Valgrind является средством динамического анализа кода, может выявлять ошибки в коде непосредственно во время выполнения программы. Поддерживает выявление: утечек, обращений в неинициализированную память, обращений по неверным адресам (в том числе переполнение буфера). Также поддерживает исполнение в режиме профилирования с помощью профайлера callgrind[100].
  • KCacheGrind — графический интерфейс для визуализации результатов профилирования, полученных с помощью профайлера callgrind[101].

Компиляторы на динамические языки и платформы

Иногда, в целях переноса тех или иных библиотек, функций и инструментов, написанных на Си, в иную среду, требуется компиляция Си-кода на язык более высокого уровня или в код виртуальной машины, предназначенной для такого языка. Следующие проекты предназначены для этих целей:

Дополнительные инструменты

Также для Си существуют и другие инструменты, облегчающие и дополняющие разработку, включая статические анализаторы и утилиты для форматирования кода. Статический анализ помогает выявлять потенциальные ошибки и уязвимости. А автоматическое форматирование кода упрощает организацию совместной работы в системах контроля версий, минимизируя конфликты из-за стилевых правок.

  • Cppcheck — статический анализатор кода для языков Си и C++ с открытыми исходными текстами, иногда выдаёт ложные срабатывания, которые можно подавлять специально оформленными комментариями в коде.
  • Clang-format — утилита командной строки для форматирования исходного кода согласно заданному стилю, который может указываться в специально оформленном файле конфигурации. Обладает множеством параметров и несколькими встроенными стилями. Разрабатывается в рамках проекта Clang[106].
  • Утилиты Indent и GNU Indent также предоставляют форматирование кода, но параметры форматирования задаются в виде опций командной строки[107].

Область применения

The C Programming Language

Язык широко применяется при разработке операционных систем, на уровне прикладного интерфейса операционных систем, во встраиваемых системах, а также для создания высокопроизводительного или критического в плане обработки ошибок кода. Одной из причин широкого распространения для программирования на низком уровне является возможность писать кроссплатформенный код, который может по-разному обрабатываться на разном оборудовании и на разных операционных системах.

Возможность писать высокопроизводительный код обеспечивается за счёт полной свободы действий программиста и отсутствия строгого контроля со стороны компилятора. Так, например, на языке Си написаны первые реализации языков Java, Python, Perl и PHP. При этом во многих программах наиболее требовательные к ресурсам части принято писать на языке Си. Ядро программы Mathematica[108] написано на Си, а MATLAB, изначально написанный на Фортране, был переписан на Си в 1984 году[109].

Также Си иногда используется как промежуточный язык при компиляции более высокоуровневых языков. Например, по такому принципу работали первые реализации языков C++, Objective-C и Go, — код, написанный на этих языках, транслировался в промежуточное представление на языке Си. Современными языками, работающими по такому же принципу, являются язык Vala и Nim.

Ещё одной областью применения языка Си являются приложения реального времени, которые требовательны по части отзывчивости кода и времени его исполнения. Такие приложения должны начинать исполнение действий в жёстко ограниченных временных рамках, а сами действия должны укладываться в определённый временной промежуток. В частности, стандарт POSIX.1 предоставляет набор функций и возможностей для создания приложений реального времени[110][111][112], однако поддержка жёсткого реального времени должна быть также реализована и со стороны операционной системы[113].

Языки-потомки

График индекса TIOBE, показывающий сравнение популярности различных языков программирования[114]

Язык Си был и остаётся одним из самых распространённых языков программирования в течение более чем сорока лет. Естественно, что его влияние можно проследить в той или иной мере во многих более поздних языках. Тем не менее среди языков, достигших определённого распространения, прямых потомков у Си немного.

Часть языков-потомков надстраивает Си дополнительными средствами и механизмами, добавляющими поддержку новых парадигм программирования (ООП, функциональное программирование, обобщённое программирование и пр.). К таким языкам относятся, прежде всего, C++ и Objective-C, а опосредованно — их потомки Swift и D. Также известны попытки улучшить Си, исправив его наиболее существенные недостатки, но сохранив его привлекательные черты. Среди них можно упомянуть исследовательский язык Cyclone (и его потомок Rust). Иногда оба направления развития объединяются в одном языке, примером может служить Go.

Отдельно необходимо упомянуть о целой группе языков, которые в большей или меньшей мере унаследовали базовый синтаксис Си (использование фигурных скобок в качестве ограничителей блоков кода, описание переменных, характерные формы операторов for, while, if, switch с параметрами в скобках, комбинированные операции ++, --, +=, -= и другие), из-за чего программы на этих языках имеют характерный внешний вид, ассоциирующийся именно с Си. Это такие языки как Java, JavaScript, PHP, Perl, AWK, C#. В действительности структура и семантика этих языков сильно отличается от Си, и обычно они предназначены для тех сфер применения, где оригинальный Си никогда не использовался.

C++

Язык программирования C++ был создан из Си и унаследовал его синтаксис, дополнив его новыми конструкциями в духе языков Simula-67, Smalltalk, Modula-2, Ada, Mesa и Clu[115]. Основными дополнениями стали поддержка ООП (описание классов, множественное наследование, полиморфизм, основанный на виртуальных функциях) и обобщённого программирования (механизм шаблонов). Но помимо этого в язык внесено множество самых различных дополнений. На данный момент C++ является одним из наиболее распространённых языков программирования в мире и позиционируется как язык общего назначения с уклоном в системное программирование[116].

Изначально C++ сохранял совместимость с Си, которая была заявлена как одно из преимуществ нового языка. Первые реализации C++ просто переводили новые конструкции в чистый Си, после чего код обрабатывался обычным Си-компилятором. Для сохранения совместимости создатели C++ отказались от исключения из него некоторых часто критикуемых особенностей Си, вместо этого создав новые, «параллельные» механизмы, которые рекомендуется применять при разработке нового кода на C++ (шаблоны вместо макроопределений, явное приведение типов вместо автоматического, контейнеры стандартной библиотеки вместо ручного динамического выделения памяти и так далее). Однако в дальнейшем языки развивались независимо, и сейчас Си и C++ последних выпущенных стандартов являются лишь частично совместимыми: не гарантируется успешная компиляция программы на Си компилятором C++, а в случае успеха нет гарантии, что откомпилированная программа будет работать правильно. Особенно неприятны некоторые тонкие семантические различия, которые могут приводить к разному поведению одного и того же кода, синтаксически корректного для обоих языков. Например, символьные константы (символы, заключённые в одинарные кавычки) имеют тип int в Си и тип char в C++, так что объём памяти, занимаемый такими константами, в разных языках различается.[117] Если программа чувствительна к размеру символьной константы, она будет работать по-разному, будучи откомпилирована трансляторами Си и C++.

Подобные различия затрудняют написание программ и библиотек, которые могли бы нормально компилироваться и работать одинаково и в Си и в C++, что, конечно, запутывает тех, кто программирует на обоих языках. Среди разработчиков и пользователей как Си, так и C++ есть сторонники максимального сокращения различий между языками, что объективно принесло бы ощутимую пользу. Существует, однако, и противоположная точка зрения, согласно которой совместимость не особенно важна, хоть и полезна, и усилия по уменьшению несовместимости не должны препятствовать улучшению каждого языка в отдельности.

Objective-C

Ещё одним вариантом расширения Си объектными средствами является язык Objective-C, созданный в 1983 году. Объектная подсистема была заимствована из Smalltalk, причём все элементы, связанные с этой подсистемой, реализованы в собственном синтаксисе, достаточно резко отличающемся от синтаксиса Си (вплоть до того, что в описании классов синтаксис объявления полей противоположен синтаксису описания переменных в Си: сначала пишется имя поля, затем его тип). В отличие от C++, Objective-C является надмножеством классического Си, то есть сохраняет совместимость с исходным языком; правильная программа на Си является правильной программой на Objective-C. Другим существенным отличием от идеологии C++ является то, что Objective-C реализует взаимодействие объектов путём обмена полноценными сообщениями, тогда как в C++ реализована концепция «отправка сообщения как вызов метода». Полноценная обработка сообщений является значительно более гибкой, к тому же она естественным образом сочетается с параллельными вычислениями. Objective-C, а также его прямой потомок Swift являются одними из самых популярных на платформах, поддерживаемых Apple.

Проблемы и критика

Язык Си уникален с той точки зрения, что именно он стал первым языком высокого уровня, всерьёз потеснившим ассемблер в разработке системного программного обеспечения. Он остаётся языком, реализованным на максимальном количестве аппаратных платформ, и одним из самых популярных языков программирования, особенно в мире свободного программного обеспечения[118]. Тем не менее язык имеет множество недостатков, он с момента появления подвергается критике многих специалистов.

Общая критика

Язык весьма сложен и наполнен опасными элементами, которые очень легко использовать неправильно. Своей структурой и правилами он никак не поддерживает программирование, нацеленное на создание надёжного и удобного в сопровождении программного кода, напротив, рождённый в эпоху прямого программирования под различные процессоры, язык способствует написанию небезопасного и запутанного кода[118]. Многие профессиональные программисты склонны считать, что язык Си мощный инструмент для создания элегантных программ, но в то же время с его помощью можно создавать крайне некачественные решения[119][120].

Из-за различных допущений в языке программы могут компилироваться со множественными ошибками, что часто приводит к непредсказуемому поведению программы. Современные компиляторы предоставляют опции для статического анализа кода[121][122], но даже они не способны выявить все возможные ошибки. Результатом неграмотного программирования на Си могут стать уязвимости программного обеспечения, что может сказаться на безопасности его использования.

У Си высокий порог вхождения[118]. Спецификация его занимает более 500 страниц текста, которые необходимо изучить полностью, так как для создания безошибочного и качественного кода приходится учитывать многие неочевидные особенности языка. Например, автоматическое приведение операндов целочисленных выражений к типу int может давать трудно предсказуемые результаты при использовании бинарных операторов[43]:

    unsigned char x = 0xFF;
    unsigned char y = (~x | 0x1) >> 1; // Интуитивно здесь ожидается 0x00
    printf("y = 0x%hhX\n", y);         // Будет выведено 0x80, если sizeof(int) > sizeof(char)

Недостаточное понимание подобных нюансов может приводить к появлению многочисленных ошибок и уязвимостей. Ещё одним фактором, увеличивающим сложность освоения Си, является отсутствие обратной связи от компилятора: язык даёт программисту полную свободу действий и позволяет компилировать программы с явными логическими ошибками. Всё это затрудняет использование Си в обучении в качестве первого языка программирования[118]

Наконец, за более чем 40 лет существования язык успел несколько устареть, и в нём достаточно проблематично использовать многие современные приёмы и парадигмы программирования.

Недостатки отдельных элементов языка

Примитивная поддержка модульности

В синтаксисе Си нет модулей и механизмов их взаимодействия. Файлы исходного кода компилируются раздельно и должны включать прототипы импортируемых из других файлов переменных, функций и типов данных. Для этого используется включение заголовочных файлов через макроподстановку #include[⇨]. В случае нарушения соответствия между файлами кода и заголовочными файлами могут возникать как ошибки этапа компоновки, так и всевозможные ошибки времени исполнения: от порчи стека и кучи до ошибок сегментирования. Поскольку директива #include лишь подставляет текст одного файла в другой, включение большого количества заголовочных файлов приводит к тому, что многократно возрастает фактический объём кода, попадающего на компиляцию, что является причиной относительно низкой скорости работы компиляторов языка Си. Необходимость согласования описаний в основном модуле и заголовочных файлах затрудняет сопровождение программы.

Предупреждения вместо ошибок

Стандарт языка даёт программисту большую свободу действий и тем самым — высокие шансы на допущение ошибок. Многое из того, что чаще всего нельзя делать, дозволено языком, и компилятор в лучшем случае выдаёт предупреждения. Хотя современные компиляторы позволяют переводить все предупреждения в класс ошибок, эта возможность используется редко, гораздо чаще предупреждения игнорируются, если программа работает удовлетворительно.

Так, например, до стандарта C99 вызов функции malloc без подключения заголовочного файла stdlib.h мог привести к порче стека, поскольку в отсутствие прототипа функция вызывалась как возвращающая тип int, тогда как фактически она возвращала тип void* (ошибка возникала, когда размеры типов на целевой платформе различались). Но даже при этом выдавалось всего лишь предупреждение.

Отсутствие контроля инициализации переменных

Автоматически и динамически создаваемые объекты по умолчанию не инициализируются и после создания содержат значения, оставшиеся в памяти от ранее находившихся там объектов. Такое значение полностью непредсказуемо, оно меняется от одной машины к другой, от запуска к запуску, от вызова функции к вызову. Если программа из-за случайного пропуска инициализации использует такое значение, то результат будет непредсказуемым и может проявиться не сразу. Современные компиляторы пытаются диагностировать эту проблему статическим анализом исходного кода, хотя в общем случае статическим анализом данную проблему решить крайне сложно. Для выявления данных проблем на этапе тестирования в ходе исполнения программы могут использоваться дополнительные инструменты: Valgrind и MemorySanitizer[123].

Отсутствие контроля над адресной арифметикой

Источником опасных ситуаций служит совместимость указателей с числовыми типами и возможность использования адресной арифметики без строгого контроля на этапах компиляции и исполнения. Это даёт возможность получить указатель на любой объект, включая исполняемый код, и обратиться по этому указателю, если только механизм защиты памяти системы этому не воспрепятствует.

Неправильное использование указателей может порождать неопределённое поведение программы и приводить к серьёзным последствиям. К примеру, указатель может быть неинициализированным или, в результате неверных арифметических операций, указывать в произвольное место памяти. На одних платформах работа с таким указателем может вызвать принудительную остановку программы, на других это может привести к порче произвольных данных в памяти; последняя ошибка опасна тем, что её последствия непредсказуемы и могут проявиться в произвольный момент времени, в том числе намного позже момента собственно ошибочного действия.

Доступ к массивам в Си также реализован посредством адресной арифметики и не предполагает средств проверки корректности обращения к элементам массива по индексу. Например, выражения a[i] и i[a] идентичны и просто транслируются к виду *(a + i), а проверка на выход за границы массива не проводится. Обращение по индексу, превышающему верхнюю границу массива, приводит к обращению к данным, размещённым в памяти после массива, что называют переполнением буфера. Когда подобное обращение происходит ошибочно, оно может привести к непредсказуемому поведению программы[56]. Нередко данная особенность используется в эксплоитах, используемых для нелегального доступа к памяти другого приложения или памяти ядра операционной системы.

Побуждающая к ошибкам динамическая память

Системные функции для работы с динамически выделяемой памятью не обеспечивают контроля за правильностью и своевременностью её выделения и освобождения, соблюдение правильного порядка работы с динамической памятью полностью возлагается на программиста. Его ошибки, соответственно, могут приводить к обращению по некорректным адресам, к преждевременному освобождению либо к утечке памяти (последнее возможно, например, если разработчик забыл вызвать free() или вызывающую free() функцию, когда это требовалось)[124].

Одной из частых ошибок является отсутствие проверок результата работы функций выделения памяти (malloc(), calloc() и прочие) на NULL, в то время как память может не выделиться, если её не хватает, или если был запрошен слишком большой объём, например, из-за приведения числа -1 , полученного в результате каких-либо ошибочных математических операций, к беззнаковому типу size_t, с последующими операциями над ним[⇦]. Ещё одной проблемой системных функций работы с памятью является неспецифицированное поведение при запросе выделения блока нулевого размера: функции могут вернуть как NULL, так и действительное значение указателя, в зависимости от конкретной реализации[125].

Некоторые конкретные реализации и сторонние библиотеки предоставляют такие средства, как подсчёт ссылок и слабые ссылки[126], умные указатели[127], а также ограниченные формы сборки мусора[128], но все эти средства не являются стандартными, что, естественно, ограничивает их применение.

Неэффективные и небезопасные строки

Для языка стандартными являются нуль-терминированные строки[⇨], соответственно все стандартные функции работают именно с ними. Это решение приводит к значительной потере эффективности за счёт малозначительной экономии памяти (по сравнению с явным хранением размера): вычисление длины строки (функция strlen()) требует обхода в цикле всей строки от начала до конца, копирование строк также сложно оптимизировать из-за наличия терминирующего нуля[47]. Из-за необходимости добавлять к данным строки терминирующий нуль становится невозможным эффективное получение подстрок в виде срезов и работа с ними как с обычными строками; выделение частей строк и манипуляции с ними обычно требуют ручного выделения и освобождения памяти, что дополнительно повышает вероятность ошибки.

Нуль-терминированные строки являются частым источником ошибок[129]. Даже стандартные функции обычно не выполняют проверки на размер целевого буфера[129] и могут не добавлять в конце строки нулевой символ[130], не говоря уже о том, что он может быть не добавлен или затёрт из-за ошибки программиста.[131].

Небезопасная реализация функций с переменным числом аргументов

Поддерживая функции с переменным числом аргументов, Си не содержит ни средств определения числа и типов фактических параметров, переданных такой функции, ни механизма безопасного доступа к ним[132]. Информирование функции о составе фактических параметров лежит на программисте, а для доступа к их значениям необходимо отсчитать правильное количество байтов от адреса последнего фиксированного параметра в стеке либо вручную, либо пользуясь набором макросов va_arg из заголовочного файла stdarg.h. При этом необходимо учитывать работу механизма автоматического неявного повышения типов при вызове функций[133], согласно которому целочисленные типы аргументов размером менее int приводятся к int (или unsigned int), а float приводится к double. Ошибка в вызове или в работе с параметрами внутри функции проявится только во время исполнения программы, приводя к непредсказуемым последствиям, от чтения неверных данных до порчи стека.

При этом стандартным средством форматированного ввода-вывода являются именно функции с переменным числом параметров (printf(), scanf() и другие), не способные проверить соответствие списка аргументов строке формата. Многие современные компиляторы проводят такую проверку для каждого их вызова, генерируя предупреждения при обнаружении несоответствия, однако в общем случае подобная проверка невозможна, так как каждая функция с переменным числом аргументов обрабатывает этот список по-своему. Невозможно статически проконтролировать даже все вызовы функции printf(), поскольку строка формата может создаваться в программе динамически.

Отсутствие унификации обработки ошибок

Синтаксис Си не включает специального механизма обработки ошибок. Стандартная библиотека поддерживает лишь простейшие средства: переменная (в случае POSIX — макрос) errno из заголовочного файла errno.h для установки кода последней ошибки и функции для получения сообщений об ошибках согласно кодам.[⇨] Такой подход приводит к необходимости писать большой объём повторяющегося кода, смешивая основной алгоритм с обработкой ошибок, к тому же он не является потокобезопасным. Причём даже в этом механизме нет единого порядка:

  • большинство функций стандартной библиотеки при ошибке возвращает маркер -1[⇨], а сам код требуется получать из errno, если функция его выставляет;
  • в стандарте POSIX принято возвращать код ошибки напрямую, но не все функции этого стандарта так делают;
  • во многих функциях, например, fopen(), fread() и fwrite(), выставление errno не стандартизировано и может отличаться в разных реализациях[78] (в POSIX требования более строгие и указаны некоторые из вариантов возможных ошибок[⇨]);
  • есть функции, у которых маркер ошибки является одним из допустимых возвращаемых значений, и перед их вызовом приходится обнулять errno, чтобы быть уверенным, что код ошибки был установлен именно этой функцией[78].

В стандартной библиотеке коды errno обозначаются через макроопределения и могут иметь одинаковые значения, что не даёт возможности анализировать коды ошибок через оператор switch. В языке нет специального типа данных для флагов и кодов ошибок, они передаются как значения типа int. Отдельный тип errno_t для хранения кода ошибки появился лишь в расширении K стандарта C11 и может не поддерживаться компиляторами[86].

Способы преодоления недостатков языка

Недостатки Си давно и хорошо известны, и с момента появления языка предпринималось множество попыток повысить качество и безопасность кода на Си, не принося в жертву его возможности.

Средства анализа корректности кода

Практически все современные компиляторы Си позволяют проводить ограниченный статический анализ кода с выдачей предупреждений о потенциальных ошибках. Также поддерживаются опции встраивания в код проверок выхода за пределы массива, разрушения стека, выхода за пределы динамической памяти, чтения неинициализированных переменных, возможностей неопределённого поведения и т. п. Однако дополнительные проверки могут сказаться на производительности итогового приложения, поэтому чаще всего их применяют только на этапе отладки.

Существуют специальные программные средства для статического анализа кода на Си для выявления не-синтаксических ошибок. Их применение не гарантирует безошибочности программ, но позволяет выявить значительную часть типичных ошибок и потенциальных уязвимостей. Максимальный эффект данных средств достигается не при эпизодическом использовании, а при применении в составе отработанной системы постоянного контроля качества кода, например, в системах непрерывной интеграции и развёртывания. Также может требоваться аннотирование кода специальными комментариями, чтобы исключить ложные срабатывания анализатора на корректных участках кода, формально попадающих под критерии ошибочных.

Стандарты безопасного программирования

Выпущено значительное количество исследований о правильном программировании на Си, от небольших статей до объёмных книг. Для поддержания качества кода на Си принимаются корпоративные и отраслевые стандарты. В частности:

  • MISRA C — стандарт, разработанный Motor Industry Software Reliability Association для использования Си в разработке встроенных систем транспортных средств. Сейчас MISRA C используется во многих отраслях, в том числе в военной, медицинской и аэрокосмической. Редакция 2013 года содержит 16 директив и 143 правила, включающие требования к коду и ограничения на использование определённых языковых средств (например, запрещено использование функций с переменным числом параметров). На рынке имеется около десятка инструментов проверки кода на соответствие MISRA C и несколько компиляторов со встроенной проверкой ограничений этого стандарта.
  • CERT C Coding Standard — стандарт, разрабатываемый координационным центром CERT[134]. Он также имеет целью обеспечение надёжного и безопасного программирования на Си. Включает правила и рекомендации для разработчиков, в том числе примеры неправильного и правильного кода по каждому отдельно взятому случаю. Стандарт используется в разработке продуктов такими компаниями как Cisco и Oracle[135].

Стандарты POSIX

Компенсации некоторых недостатков языка способствует набор стандартов POSIX. Стандартизируется установка errno многими функциями, позволяя обрабатывать ошибки, возникающие, например, в функциях работы с файлами, а также вводятся потокобезопасные аналоги некоторых функций стандартной библиотеки, безопасные варианты которых в стандарте языка присутствуют лишь в расширении K[136].

См. также

Примечания

Комментарии

  1. B — вторая буква английского алфавита, а C — третья буква английского алфавита.
  2. Макрос bool из заголовочного файла stdbool.h является обёрткой над ключевым словом _Bool.
  3. Макрос complex из заголовочного файла complex.h является обёрткой над ключевым словом _Complex.
  4. Макрос imaginary из заголовочного файла complex.h является обёрткой над ключевым словом _Imaginary.
  5. Макрос alignas из заголовочного файла stdalign.h является обёрткой над ключевым словом _Alignas.
  6. 6,0 6,1 6,2 Макрос alignof из заголовочного файла stdalign.h является обёрткой над ключевым словом _Alignof.
  7. Макрос noreturn из заголовочного файла stdnoreturn.h является обёрткой над ключевым словом _Noreturn.
  8. Макрос static_assert из заголовочного файла assert.h является обёрткой над ключевым словом _Static_assert.
  9. Макрос thread_local из заголовочного файла threads.h является обёрткой над ключевым словом _Thread_local.
  10. 10,0 10,1 10,2 10,3 10,4 10,5 10,6 Первое появление знаковых и беззнаковых типов char, short, int и long было в K&R C.
  11. 11,0 11,1 Соответствие формата типов float и double стандарту IEC 60559 определяется расширением F стандарта Си, поэтому формат может отличаться на отдельных платформах или компиляторах.

Источники

  1. Rui Ueyama. How I wrote a self-hosting C compiler in 40 days (англ.). www.sigbus.info (декабрь 2015). Дата обращения: 18 февраля 2019. Архивировано 23 марта 2019 года.
  2. A garbage collector for C and C++ Архивная копия от 13 октября 2005 на Wayback Machine (англ.)
  3. Object-Oriented Programming With ANSI-C Архивная копия от 6 марта 2016 на Wayback Machine (англ.)
  4. Instantiable classed types: objects (англ.). GObject Reference Manual. developer.gnome.org. Дата обращения: 27 мая 2019. Архивировано 27 мая 2019 года.
  5. Non-instantiable classed types: interfaces (англ.). GObject Reference Manual. developer.gnome.org. Дата обращения: 27 мая 2019. Архивировано 27 мая 2019 года.
  6. 6,0 6,1 Черновик стандарта C17, 5.2.1 Character sets, с. 17.
  7. 7,0 7,1 Черновик стандарта C17, 6.4.2 Identifiers, с. 43—44.
  8. Черновик стандарта C17, 6.4.4 Constants, с. 45—50.
  9. 9,0 9,1 Подбельский, Фомин, 2012, с. 19.
  10. 10,0 10,1 Черновик стандарта C17, 6.4.4.1 Integer constants, с. 46.
  11. Черновик стандарта C17, 6.4.4.2 Floating constants, с. 47—48.
  12. 12,0 12,1 Черновик стандарта C17, 6.4.4.4 Character constants, с. 49—50.
  13. STR30-C. Do not attempt to modify string literals - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 27 мая 2019. Архивировано 27 мая 2019 года.
  14. Черновик стандарта C17, 6.4.5 String literals, с. 50—52.
  15. Clang-Format Style Options — Clang 9 documentation (англ.). clang.llvm.org. Дата обращения: 19 мая 2019. Архивировано 20 мая 2019 года.
  16. 16,0 16,1 16,2 16,3 DCL06-C. Use meaningful symbolic constants to represent literal values - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 6 февраля 2019. Архивировано 7 февраля 2019 года.
  17. 17,0 17,1 Черновик стандарта C17, с. 84.
  18. Черновик стандарта C17, 6.4.1 Keywords, с. 42.
  19. 19,0 19,1 Free Software Foundation (FSF). Status of C99 features in GCC (англ.). GNU Project. gcc.gnu.org. Дата обращения: 31 мая 2019. Архивировано 3 июня 2019 года.
  20. 20,0 20,1 20,2 20,3 Черновик стандарта C17, 7.1.3 Reserved identifiers, с. 132.
  21. Черновик стандарта C17, 6.5.3 Unary operators, с. 63—65.
  22. Черновик стандарта C17, 6.5 Expressions, с. 66—72.
  23. Черновик стандарта C17, 6.5.16 Assignment operators, с. 72—74.
  24. Черновик стандарта C17, с. 55—75.
  25. 25,0 25,1 The GNU C Reference Manual. 3.19 Operator Precedence (англ.). www.gnu.org. Дата обращения: 13 февраля 2019. Архивировано 7 февраля 2019 года.
  26. 26,0 26,1 26,2 26,3 26,4 EXP30-C. Do not depend on the order of evaluation for side effects - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 14 февраля 2019. Архивировано 15 февраля 2019 года.
  27. 27,0 27,1 BB. Definitions - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 16 февраля 2019. Архивировано 16 февраля 2019 года.
  28. Подбельский, Фомин, 2012, 1.4. Операции, с. 42.
  29. Подбельский, Фомин, 2012, 2.3. Операторы цикла, с. 78.
  30. 30,0 30,1 EXP19-C. Use braces for the body of an if, for, or while statement - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 2 июня 2019. Архивировано 2 июня 2019 года.
  31. Dynamically Loaded (DL) Libraries (англ.). tldp.org. Дата обращения: 18 февраля 2019. Архивировано 12 ноября 2020 года.
  32. 32,0 32,1 Черновик стандарта C17, 6.7.4 Function specifiers, с. 90—91.
  33. PRE00-C. Prefer inline or static functions to function-like macros - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 4 июня 2019. Архивировано 7 августа 2021 года.
  34. Черновик стандарта C17, 6.11 Future language directions, с. 130.
  35. Does C support function overloading? | GeeksforGeeks. Дата обращения: 15 декабря 2013. Архивировано 15 декабря 2013 года.
  36. The GNU C Reference Manual. www.gnu.org. Дата обращения: 21 мая 2017. Архивировано 27 апреля 2021 года.
  37. Width of Type (The GNU C Library) (англ.). www.gnu.org. Дата обращения: 7 декабря 2018. Архивировано 9 декабря 2018 года.
  38. Черновик стандарта C17, 6.2.5 Types, с. 31.
  39. 39,0 39,1 Joint Technical Committee ISO/IEC JTC 1. ISO/IEC 9899:201x. Programming languages — C. — ISO/IEC, 2011. — С. 14. — 678 с. Архивная копия от 30 мая 2017 на Wayback Machine
  40. Check 0.10.0: 4. Advanced Features (англ.). Check. check.sourceforge.net. Дата обращения: 11 февраля 2019. Архивировано 18 мая 2018 года.
  41. Type Conversion Macros: GLib Reference Manual (англ.). developer.gnome.org. Дата обращения: 14 января 2019. Архивировано 14 января 2019 года.
  42. INT01-C. Use rsize_t or size_t for all integer values representing the size of an object - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 22 февраля 2019. Архивировано 7 августа 2021 года.
  43. 43,0 43,1 43,2 INT02-C. Understand integer conversion rules - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 22 февраля 2019. Архивировано 22 февраля 2019 года.
  44. FLP02-C. Avoid using floating-point numbers when precise computation is needed - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 21 мая 2019. Архивировано 7 августа 2021 года.
  45. 45,0 45,1 Черновик стандарта C17, IEC 60559 floating-point arithmetic, с. 370.
  46. Черновик стандарта C17, 7.12 Mathematics <math.h>, с. 169—170.
  47. 47,0 47,1 Poul-Henning Kamp. The Most Expensive One-byte Mistake - ACM Queue (англ.). queue.acm.org (25 июля 2011). Дата обращения: 28 мая 2019. Архивировано 30 апреля 2019 года.
  48. 48,0 48,1 unicode(7) - Linux manual page (англ.). man7.org. Дата обращения: 24 февраля 2019. Архивировано 25 февраля 2019 года.
  49. 49,0 49,1 49,2 The wchar_t mess - GNU libunistring (англ.). www.gnu.org. Дата обращения: 2 января 2019. Архивировано 17 сентября 2019 года.
  50. Programming with wide characters (англ.). Linux.com | The source for Linux information (11 февраля 2006). Дата обращения: 7 июня 2019. Архивировано 7 июня 2019 года.
  51. Markus Kuhn. UTF-8 and Unicode FAQ (англ.). www.cl.cam.ac.uk. Дата обращения: 25 февраля 2019. Архивировано 27 февраля 2019 года.
  52. Defect Report Summary for C11. www.open-std.org. Дата обращения: 2 января 2019. Архивировано 1 января 2019 года.
  53. Standard Enumerations: GTK+ 3 Reference Manual (англ.). developer.gnome.org. Дата обращения: 15 января 2019. Архивировано 14 января 2019 года.
  54. Object properties: GObject Reference Manual (англ.). developer.gnome.org. Дата обращения: 15 января 2019. Архивировано 16 января 2019 года.
  55. Using the GNU Compiler Collection (GCC): Common Type Attributes (англ.). gcc.gnu.org. Дата обращения: 19 января 2019. Архивировано 16 января 2019 года.
  56. 56,0 56,1 ARR00-C. Understand how arrays work - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 30 мая 2019. Архивировано 30 мая 2019 года.
  57. ARR32-C. Ensure size arguments for variable length arrays are in a valid range - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 18 февраля 2019. Архивировано 19 февраля 2019 года.
  58. Черновик стандарта C17, 6.7.9 Initialization, с. 101.
  59. DCL38-C. Use the correct syntax when declaring a flexible array member - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 21 февраля 2019. Архивировано 22 февраля 2019 года.
  60. OpenSSL_version (англ.). www.openssl.org. Дата обращения: 9 декабря 2018. Архивировано 9 декабря 2018 года.
  61. Version Information: GTK+ 3 Reference Manual (англ.). developer.gnome.org. Дата обращения: 9 декабря 2018. Архивировано 16 ноября 2018 года.
  62. PRE10-C. Wrap multistatement macros in a do-while loop - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 9 декабря 2018. Архивировано 9 декабря 2018 года.
  63. PRE01-C. Use parentheses within macros around parameter names - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 9 декабря 2018. Архивировано 9 декабря 2018 года.
  64. PRE06-C. Enclose header files in an include guard - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 25 мая 2019. Архивировано 25 мая 2019 года.
  65. 65,0 65,1 Черновик стандарта C17, 5.1.2.2 Hosted environment, с. 10—11.
  66. 66,0 66,1 66,2 Черновик стандарта C17, 6.2.4 Storage durations of objects, с. 30.
  67. 67,0 67,1 Черновик стандарта C17, 7.22.4.4 The exit function, с. 256.
  68. MEM05-C. Avoid large stack allocations - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 24 мая 2019. Архивировано 24 мая 2019 года.
  69. Черновик стандарта C17, 6.7.1 Storage-class specifiers, с. 79.
  70. Черновик стандарта C17, 6.7.6.3 Function declarators (including prototypes), с. 96.
  71. Указатели в C абстрактнее, чем может показаться. www.viva64.com. Дата обращения: 30 декабря 2018. Архивировано 30 декабря 2018 года.
  72. Таненбаум Эндрю С, Бос Херберт. Современные операционные системы. 4-е изд. — СПб.: Издательский дом "Питер", 2019. — С. 828. — 1120 с. — (Классика "Computer science"). — ISBN 9785446111558. Архивная копия от 7 августа 2021 на Wayback Machine
  73. Jonathan Corbet. Ripples from Stack Clash (англ.). lwn.net (28 июня 2017). Дата обращения: 25 мая 2019. Архивировано 25 мая 2019 года.
  74. Hardening ELF binaries using Relocation Read-Only (RELRO) (англ.). www.redhat.com. Дата обращения: 25 мая 2019. Архивировано 25 мая 2019 года.
  75. Traditional Process Address Space — Static Program (англ.). www.openbsd.org. Дата обращения: 4 марта 2019. Архивировано 8 декабря 2019 года.
  76. Dr Thabang Mokoteli. ICMLG 2017 5th International Conference on Management Leadership and Governance. — Academic Conferences and publishing limited, 2017-03. — С. 42. — 567 с. — ISBN 9781911218289. Архивная копия от 7 августа 2021 на Wayback Machine
  77. Traditional Process Address Space — Program w/Shared Libs (англ.). www.openbsd.org. Дата обращения: 4 марта 2019. Архивировано 8 декабря 2019 года.
  78. 78,0 78,1 78,2 78,3 ERR30-C. Set errno to zero before calling a library function known to set errno, and check errno only after the function returns a value indicating failure - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 23 мая 2019. Архивировано 19 ноября 2018 года.
  79. 79,0 79,1 79,2 79,3 79,4 79,5 ERR33-C. Detect and handle standard library errors - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 23 мая 2019. Архивировано 23 мая 2019 года.
  80. sys_types.h.0p - Linux manual page (англ.). man7.org. Дата обращения: 23 мая 2019. Архивировано 23 мая 2019 года.
  81. 81,0 81,1 ERR02-C. Avoid in-band error indicators - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 4 января 2019. Архивировано 5 января 2019 года.
  82. FIO34-C. Distinguish between characters read from a file and EOF or WEOF - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 4 января 2019. Архивировано 4 января 2019 года.
  83. Coding Style (англ.). The systemd System and Service Manager. github.com. Дата обращения: 1 февраля 2019. Архивировано 31 декабря 2020 года.
  84. Error Reporting: GLib Reference Manual (англ.). developer.gnome.org. Дата обращения: 1 февраля 2019. Архивировано 2 февраля 2019 года.
  85. Eina: Error (англ.). docs.enlightenment.org. Дата обращения: 1 февраля 2019. Архивировано 2 февраля 2019 года.
  86. 86,0 86,1 DCL09-C. Declare functions that return errno with a return type of errno_t - SEI CERT C Coding Standard - Confluence. wiki.sei.cmu.edu. Дата обращения: 21 декабря 2018. Архивировано 21 декабря 2018 года.
  87. 87,0 87,1 FLP32-C. Prevent or detect domain and range errors in math functions - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 5 января 2019. Архивировано 5 января 2019 года.
  88. 88,0 88,1 MEM12-C. Consider using a goto chain when leaving a function on error when using and releasing resources - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 4 января 2019. Архивировано 5 января 2019 года.
  89. ERR04-C. Choose an appropriate termination strategy - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 4 января 2019. Архивировано 5 января 2019 года.
  90. MEM31-C. Free dynamically allocated memory when no longer needed - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 6 января 2019. Архивировано 6 января 2019 года.
  91. Using the GNU Compiler Collection (GCC): Standards (англ.). gcc.gnu.org. Дата обращения: 23 февраля 2019. Архивировано 17 июня 2012 года.
  92. Language Compatibility (англ.). clang.llvm.org. Дата обращения: 23 февраля 2019. Архивировано 19 февраля 2019 года.
  93. Clang 6.0.0 Release Notes — Clang 6 documentation. releases.llvm.org. Дата обращения: 23 февраля 2019. Архивировано 23 февраля 2019 года.
  94. Siddhesh Poyarekar - The GNU C Library version 2.29 is now available (англ.). sourceware.org. Дата обращения: 2 февраля 2019. Архивировано 2 февраля 2019 года.
  95. Alpine Linux has switched to musl libc | Alpine Linux (англ.). alpinelinux.org. Дата обращения: 2 февраля 2019. Архивировано 3 февраля 2019 года.
  96. musl - Void Linux Handbook. docs.voidlinux.org. Дата обращения: 29 января 2022. Архивировано 9 декабря 2021 года.
  97. Особенности библиотеки CRT. docs.microsoft.com. Дата обращения: 2 февраля 2019. Архивировано 7 августа 2021 года.
  98. Supported Languages - Features | CLion (англ.). JetBrains. Дата обращения: 23 февраля 2019. Архивировано 25 марта 2019 года.
  99. 99,0 99,1 Check 0.10.0: 2. Unit Testing in C (англ.). check.sourceforge.net. Дата обращения: 23 февраля 2019. Архивировано 5 июня 2018 года.
  100. 6. Callgrind: a call-graph generating cache and branch prediction profiler (англ.). Valgrind Documentation. valgrind.org. Дата обращения: 21 мая 2019. Архивировано 23 мая 2019 года.
  101. KCachegrind. kcachegrind.sourceforge.net. Дата обращения: 21 мая 2019. Архивировано 6 апреля 2019 года.
  102. Emscripten LLVM-to-JavaScript compiler. Дата обращения: 25 сентября 2012. Архивировано 17 декабря 2012 года.
  103. Flash C++ Compiler. Дата обращения: 25 января 2013. Архивировано 25 мая 2013 года.
  104. Проект Clue на сайте SourceForge.net
  105. Axiomatic Solutions Sdn Bhd. Дата обращения: 7 марта 2009. Архивировано 23 февраля 2009 года.
  106. ClangFormat — Clang 9 documentation (англ.). clang.llvm.org. Дата обращения: 5 марта 2019. Архивировано 6 марта 2019 года.
  107. indent(1) - Linux man page (англ.). linux.die.net. Дата обращения: 5 марта 2019. Архивировано 13 мая 2019 года.
  108. Wolfram Research, Inc. SYSTEMS INTERFACES AND DEPLOYMENT (англ.). Wolfram Mathematica® Tutorial Collection 36—37. library.wolfram.com (2008). Дата обращения: 29 мая 2019. Архивировано 6 сентября 2015 года.
  109. Cleve Moler. The Growth of MATLAB and The MathWorks over Two Decades. TheMathWorks News&Notes. www.mathworks.com (январь 2006). Дата обращения: 29 мая 2019. Архивировано 4 марта 2016 года.
  110. sched_setscheduler (англ.). pubs.opengroup.org. Дата обращения: 4 февраля 2019. Архивировано 24 февраля 2019 года.
  111. clock_gettime (англ.). pubs.opengroup.org. Дата обращения: 4 февраля 2019. Архивировано 24 февраля 2019 года.
  112. clock_nanosleep (англ.). pubs.opengroup.org. Дата обращения: 4 февраля 2019. Архивировано 24 февраля 2019 года.
  113. М. Джонс. Анатомия Linux-архитектур реального времени. www.ibm.com (30 октября 2008). Дата обращения: 4 февраля 2019. Архивировано 7 февраля 2019 года.
  114. TIOBE Index (англ.). www.tiobe.com. Дата обращения: 2 февраля 2019. Архивировано 25 февраля 2018 года.
  115. Stroustrup, Bjarne Evolving a language in and for the real world: C++ 1991-2006. Дата обращения: 9 июля 2018. Архивировано 20 ноября 2007 года.
  116. Stroustrup: FAQ. www.stroustrup.com. Дата обращения: 3 июня 2019. Архивировано 6 февраля 2016 года.
  117. Annex 0: Compatibility. 1.2. C++ and ISO C. Working Paper for Draft Proposed International Standard for Information Systems — Programming Language C++ (2 декабря 1996). — см. 1.2.1p3 (параграф 3 в разделе 1.2.1). Дата обращения: 6 июня 2009. Архивировано 22 августа 2011 года.
  118. 118,0 118,1 118,2 118,3 Столяров, 2010, 1. Предисловие, p. 79.
  119. Летопись языков. Си. Издательство «Открытые системы». Дата обращения: 8 декабря 2018. Архивировано 9 декабря 2018 года.
  120. Allen I. Holub. Enough Rope to Shoot Yourself in the Foot: Rules for C and C++ Programming. — McGraw-Hill, 1995. — 214 с. — ISBN 9780070296893. Архивная копия от 9 декабря 2018 на Wayback Machine
  121. Using the GNU Compiler Collection (GCC): Warning Options. gcc.gnu.org. Дата обращения: 8 декабря 2018. Архивировано 5 декабря 2018 года.
  122. Diagnostic flags in Clang — Clang 8 documentation. clang.llvm.org. Дата обращения: 8 декабря 2018. Архивировано 9 декабря 2018 года.
  123. MemorySanitizer — Clang 8 documentation (англ.). clang.llvm.org. Дата обращения: 8 декабря 2018. Архивировано 1 декабря 2018 года.
  124. MEM00-C. Allocate and free memory in the same module, at the same level of abstraction - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 4 июня 2019. Архивировано 4 июня 2019 года.
  125. MEM04-C. Beware of zero-length allocations - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 11 января 2019. Архивировано 12 января 2019 года.
  126. Object memory management: GObject Reference Manual. developer.gnome.org. Дата обращения: 9 декабря 2018. Архивировано 7 сентября 2018 года.
  127. Например, snai.pe c-smart-pointers Архивная копия от 14 августа 2018 на Wayback Machine
  128. Garbage Collection in C Programs. Дата обращения: 16 мая 2019. Архивировано 27 марта 2019 года.
  129. 129,0 129,1 CERN Computer Security Information. security.web.cern.ch. Дата обращения: 12 января 2019. Архивировано 5 января 2019 года.
  130. CWE - CWE-170: Improper Null Termination (3.2) (англ.). cwe.mitre.org. Дата обращения: 12 января 2019. Архивировано 13 января 2019 года.
  131. STR32-C. Do not pass a non-null-terminated character sequence to a library function that expects a string - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 12 января 2019. Архивировано 13 января 2019 года.
  132. DCL50-CPP. Do not define a C-style variadic function - SEI CERT C++ Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 25 мая 2019. Архивировано 25 мая 2019 года.
  133. EXP47-C. Do not call va_arg with an argument of the incorrect type - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 8 декабря 2018. Архивировано 9 декабря 2018 года.
  134. SEI CERT C Coding Standard - SEI CERT C Coding Standard - Confluence. wiki.sei.cmu.edu. Дата обращения: 9 декабря 2018. Архивировано 8 декабря 2018 года.
  135. Introduction - SEI CERT C Coding Standard - Confluence. wiki.sei.cmu.edu. Дата обращения: 24 мая 2019. Архивировано 24 мая 2019 года.
  136. CON33-C. Avoid race conditions when using library functions - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 23 января 2019. Архивировано 23 января 2019 года.

Литература

  • ISO/IEC. ISO/IEC9899:2017. Programming languages — C (недоступная ссылка). www.open-std.org (2017). Дата обращения: 3 декабря 2018. Архивировано 24 октября 2018 года.
  • Керниган Б., Ритчи Д. Язык программирования Си = The C programming language. — 2-е изд. — М.: Вильямс, 2007. — С. 304. — ISBN 0-13-110362-8.
  • Гукин Д. Язык программирования Си для «чайников» = C For Dummies. — М.: Диалектика, 2006. — С. 352. — ISBN 0-7645-7068-4.
  • Подбельский В. В., Фомин С. С. Курс программирования на языке Си: учебник. — М.: ДМК Пресс, 2012. — 318 с. — ISBN 978-5-94074-449-8.
  • Прата С. Язык программирования С: Лекции и упражнения = C Primer Plus. — М.: Вильямс, 2006. — С. 960. — ISBN 5-8459-0986-4.
  • Прата С. Язык программирования C (C11). Лекции и упражнения, 6-е издание = C Primer Plus, 6th Edition. — М.: Вильямс, 2015. — 928 с. — ISBN 978-5-8459-1950-2.
  • Столяров А. В. Язык Си и начальное обучение программированию // Сборник статей молодых учёных факультета ВМК МГУ. — Издательский отдел факультета ВМК МГУ, 2010. — № 7. — С. 78—90.
  • Шилдт Г. C: полное руководство, классическое издание = C: The Complete Reference, 4th Edition. — М.: Вильямс, 2010. — С. 704. — ISBN 978-5-8459-1709-6.
  • Языки программирования Ада, Си, Паскаль = Comparing and Assessong Programming Languages Ada, C, and Pascal / А. Фьюэр, Н. Джехани. — М.: Радио и Саязь, 1989. — 368 с. — 50 000 экз. — ISBN 5-256-00309-7.

Ссылки